← All changes
|
includes/abilities/class-mainwp-abilities-clients.php
+226
-13
6.2.3
→
6.3
View file →
| @@ -159,8 +159,12 @@ | ||
| 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 | + | |
| 163 | 167 | $input = MainWP_Abilities_Util::normalize_input( |
| 164 | 168 | $input, |
| 165 | 169 | array( |
| 166 | 170 | 'page' => 1, |
| @@ -194,8 +198,26 @@ | ||
| 194 | 198 | if ( ! empty( $input['exclude'] ) ) { |
| 195 | 199 | $args['exclude'] = $input['exclude']; |
| 196 | 200 | } |
| 197 | 201 | |
| 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 | + | |
| 198 | 220 | $db = MainWP_DB_Client::instance(); |
| 199 | 221 | $clients = $db->get_wp_clients( $args ); |
| 200 | 222 | $total = $db->get_wp_clients( array_merge( $args, array( 'count_only' => true ) ) ); |
| 201 | 223 | |
| @@ -275,10 +297,22 @@ | ||
| 275 | 297 | * @param array $input Validated input parameters (unused but required by ability signature). |
| 276 | 298 | * @return array|\WP_Error Result array or error. |
| 277 | 299 | */ |
| 278 | 300 | public static function execute_count_clients( array $input ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found -- NOSONAR Required by ability signature. |
| 279 | - $total = MainWP_DB_Client::instance()->get_wp_clients( array( 'count_only' => true ) ); | |
| 301 | + $allowed_sites = MainWP_Abilities_Util::get_allowed_site_ids(); | |
| 280 | 302 | |
| 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 | + | |
| 281 | 315 | return array( |
| 282 | 316 | 'total' => (int) $total, |
| 283 | 317 | ); |
| 284 | 318 | } |
| @@ -350,8 +384,14 @@ | ||
| 350 | 384 | if ( is_wp_error( $client ) ) { |
| 351 | 385 | return $client; |
| 352 | 386 | } |
| 353 | 387 | |
| 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 | + | |
| 354 | 394 | return MainWP_Abilities_Util::format_client_for_output( $client ); |
| 355 | 395 | } |
| 356 | 396 | |
| 357 | 397 | /** |
| @@ -368,9 +408,9 @@ | ||
| 368 | 408 | 'category' => 'mainwp-clients', |
| 369 | 409 | 'input_schema' => self::get_add_client_input_schema(), |
| 370 | 410 | 'output_schema' => self::get_client_output_schema(), |
| 371 | 411 | 'execute_callback' => array( self::class, 'execute_add_client' ), |
| 372 | - 'permission_callback' => array( MainWP_Abilities_Util::class, 'check_manage_sites_permission' ), | |
| 412 | + 'permission_callback' => MainWP_Abilities_Util::make_operation_permission_callback( 'mainwp/add-client-v1', array( MainWP_Abilities_Util::class, 'check_manage_sites_permission' ) ), | |
| 373 | 413 | 'meta' => array( |
| 374 | 414 | 'show_in_rest' => true, |
| 375 | 415 | 'annotations' => array( |
| 376 | 416 | 'instructions' => '', |
| @@ -418,8 +458,44 @@ | ||
| 418 | 458 | ); |
| 419 | 459 | } |
| 420 | 460 | |
| 421 | 461 | /** |
| 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 | + /** | |
| 422 | 498 | * Execute add-client ability. |
| 423 | 499 | * |
| 424 | 500 | * @param array $input Validated input parameters. |
| 425 | 501 | * @return array|\WP_Error Result array or error. |
| @@ -424,8 +500,15 @@ | ||
| 424 | 500 | * @param array $input Validated input parameters. |
| 425 | 501 | * @return array|\WP_Error Result array or error. |
| 426 | 502 | */ |
| 427 | 503 | 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 | + | |
| 428 | 511 | if ( empty( $input['name'] ) ) { |
| 429 | 512 | return new \WP_Error( |
| 430 | 513 | 'mainwp_invalid_input', |
| 431 | 514 | __( 'Client name is required.', 'mainwp' ), |
| @@ -484,9 +567,9 @@ | ||
| 484 | 567 | 'category' => 'mainwp-clients', |
| 485 | 568 | 'input_schema' => self::get_update_client_input_schema(), |
| 486 | 569 | 'output_schema' => self::get_client_output_schema(), |
| 487 | 570 | 'execute_callback' => array( self::class, 'execute_update_client' ), |
| 488 | - 'permission_callback' => array( MainWP_Abilities_Util::class, 'check_manage_sites_permission' ), | |
| 571 | + 'permission_callback' => MainWP_Abilities_Util::make_operation_permission_callback( 'mainwp/update-client-v1', array( MainWP_Abilities_Util::class, 'check_manage_sites_permission' ) ), | |
| 489 | 572 | 'meta' => array( |
| 490 | 573 | 'show_in_rest' => true, |
| 491 | 574 | 'annotations' => array( |
| 492 | 575 | 'instructions' => '', |
| @@ -547,8 +630,46 @@ | ||
| 547 | 630 | if ( is_wp_error( $client ) ) { |
| 548 | 631 | return $client; |
| 549 | 632 | } |
| 550 | 633 | |
| 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 | + | |
| 551 | 672 | $data = $input; |
| 552 | 673 | $data['client_id'] = $client->client_id; |
| 553 | 674 | |
| 554 | 675 | unset( $data['client_id_or_email'] ); |
| @@ -594,9 +715,9 @@ | ||
| 594 | 715 | 'category' => 'mainwp-clients', |
| 595 | 716 | 'input_schema' => self::get_delete_client_input_schema(), |
| 596 | 717 | 'output_schema' => self::get_delete_client_output_schema(), |
| 597 | 718 | 'execute_callback' => array( self::class, 'execute_delete_client' ), |
| 598 | - 'permission_callback' => array( MainWP_Abilities_Util::class, 'check_manage_sites_permission' ), | |
| 719 | + 'permission_callback' => MainWP_Abilities_Util::make_operation_permission_callback( 'mainwp/delete-client-v1', array( MainWP_Abilities_Util::class, 'check_manage_sites_permission' ) ), | |
| 599 | 720 | 'meta' => array( |
| 600 | 721 | 'show_in_rest' => true, |
| 601 | 722 | 'annotations' => array( |
| 602 | 723 | 'instructions' => 'Destructive operation requires confirm: true. Only call when user explicitly requests deletion.', |
| @@ -685,9 +806,12 @@ | ||
| 685 | 806 | 'dry_run' => false, |
| 686 | 807 | ) |
| 687 | 808 | ); |
| 688 | 809 | |
| 689 | - if ( $input['dry_run'] && $input['confirm'] ) { | |
| 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 ) { | |
| 690 | 814 | return new \WP_Error( |
| 691 | 815 | 'mainwp_invalid_input', |
| 692 | 816 | __( 'Cannot specify both dry_run and confirm. Use dry_run for preview, confirm for actual deletion.', 'mainwp' ), |
| 693 | 817 | array( 'status' => 400 ) |
| @@ -699,9 +823,17 @@ | ||
| 699 | 823 | if ( is_wp_error( $client ) ) { |
| 700 | 824 | return $client; |
| 701 | 825 | } |
| 702 | 826 | |
| 703 | - if ( $input['dry_run'] ) { | |
| 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 ) { | |
| 704 | 836 | $sites = MainWP_DB_Client::instance()->get_websites_by_client_ids( $client->client_id ); |
| 705 | 837 | $sites_count = is_array( $sites ) ? count( $sites ) : 0; |
| 706 | 838 | |
| 707 | 839 | $warnings = array(); |
| @@ -728,9 +860,9 @@ | ||
| 728 | 860 | 'warnings' => $warnings, |
| 729 | 861 | ); |
| 730 | 862 | } |
| 731 | 863 | |
| 732 | - if ( ! $input['confirm'] ) { | |
| 864 | + if ( ! $confirm ) { | |
| 733 | 865 | return new \WP_Error( |
| 734 | 866 | 'mainwp_confirmation_required', |
| 735 | 867 | __( 'This is a destructive operation. Set confirm: true or use dry_run: true for preview.', 'mainwp' ), |
| 736 | 868 | array( 'status' => 400 ) |
| @@ -820,13 +952,29 @@ | ||
| 820 | 952 | if ( is_wp_error( $client ) ) { |
| 821 | 953 | return $client; |
| 822 | 954 | } |
| 823 | 955 | |
| 824 | - $sites = MainWP_DB_Client::instance()->get_websites_by_client_ids( $client->client_id ); | |
| 956 | + $access = MainWP_Abilities_Util::check_client_access( $client, $input ); | |
| 957 | + if ( is_wp_error( $access ) ) { | |
| 958 | + return $access; | |
| 959 | + } | |
| 825 | 960 | |
| 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 | + | |
| 826 | 970 | $items = array(); |
| 827 | 971 | if ( is_array( $sites ) ) { |
| 828 | 972 | 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 | + } | |
| 829 | 977 | $items[] = MainWP_Abilities_Util::format_site_for_output( $site ); |
| 830 | 978 | } |
| 831 | 979 | } |
| 832 | 980 | |
| @@ -906,11 +1054,32 @@ | ||
| 906 | 1054 | if ( is_wp_error( $client ) ) { |
| 907 | 1055 | return $client; |
| 908 | 1056 | } |
| 909 | 1057 | |
| 910 | - $sites = MainWP_DB_Client::instance()->get_websites_by_client_ids( $client->client_id ); | |
| 911 | - $count = is_array( $sites ) ? count( $sites ) : 0; | |
| 1058 | + $access = MainWP_Abilities_Util::check_client_access( $client, $input ); | |
| 1059 | + if ( is_wp_error( $access ) ) { | |
| 1060 | + return $access; | |
| 1061 | + } | |
| 912 | 1062 | |
| 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 | + | |
| 913 | 1082 | return array( |
| 914 | 1083 | 'total' => $count, |
| 915 | 1084 | ); |
| 916 | 1085 | } |
| @@ -1012,8 +1181,13 @@ | ||
| 1012 | 1181 | if ( is_wp_error( $client ) ) { |
| 1013 | 1182 | return $client; |
| 1014 | 1183 | } |
| 1015 | 1184 | |
| 1185 | + $access = MainWP_Abilities_Util::check_client_access( $client, $input ); | |
| 1186 | + if ( is_wp_error( $access ) ) { | |
| 1187 | + return $access; | |
| 1188 | + } | |
| 1189 | + | |
| 1016 | 1190 | if ( ! class_exists( 'MainWP\Dashboard\Module\CostTracker\Cost_Tracker_DB' ) ) { |
| 1017 | 1191 | return new \WP_Error( |
| 1018 | 1192 | 'mainwp_internal_error', |
| 1019 | 1193 | __( 'Cost Tracker database class is not available.', 'mainwp' ), |
| @@ -1021,13 +1195,38 @@ | ||
| 1021 | 1195 | ); |
| 1022 | 1196 | } |
| 1023 | 1197 | |
| 1024 | 1198 | $cost_db = \MainWP\Dashboard\Module\CostTracker\Cost_Tracker_DB::get_instance(); |
| 1025 | - $costs = $cost_db->get_all_cost_trackers_by_clients( array( $client->client_id ) ); | |
| 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(); | |
| 1026 | 1204 | |
| 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 | + | |
| 1027 | 1211 | $items = array(); |
| 1028 | 1212 | if ( is_array( $costs ) ) { |
| 1029 | 1213 | 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 | + } | |
| 1030 | 1229 | $items[] = MainWP_Abilities_Util::format_cost_for_output( $cost ); |
| 1031 | 1230 | } |
| 1032 | 1231 | } |
| 1033 | 1232 | |
| @@ -1051,9 +1250,9 @@ | ||
| 1051 | 1250 | 'category' => 'mainwp-clients', |
| 1052 | 1251 | 'input_schema' => self::get_suspend_client_input_schema(), |
| 1053 | 1252 | 'output_schema' => self::get_client_output_schema(), |
| 1054 | 1253 | 'execute_callback' => array( self::class, 'execute_suspend_client' ), |
| 1055 | - 'permission_callback' => array( MainWP_Abilities_Util::class, 'check_manage_sites_permission' ), | |
| 1254 | + 'permission_callback' => MainWP_Abilities_Util::make_operation_permission_callback( 'mainwp/suspend-client-v1', array( MainWP_Abilities_Util::class, 'check_manage_sites_permission' ) ), | |
| 1056 | 1255 | 'meta' => array( |
| 1057 | 1256 | 'show_in_rest' => true, |
| 1058 | 1257 | 'annotations' => array( |
| 1059 | 1258 | 'instructions' => '', |
| @@ -1093,8 +1292,15 @@ | ||
| 1093 | 1292 | if ( is_wp_error( $client ) ) { |
| 1094 | 1293 | return $client; |
| 1095 | 1294 | } |
| 1096 | 1295 | |
| 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 | + | |
| 1097 | 1303 | $updated = MainWP_DB_Client::instance()->update_client( |
| 1098 | 1304 | array( |
| 1099 | 1305 | 'client_id' => $client->client_id, |
| 1100 | 1306 | 'suspended' => 1, |
| @@ -1136,9 +1342,9 @@ | ||
| 1136 | 1342 | 'category' => 'mainwp-clients', |
| 1137 | 1343 | 'input_schema' => self::get_unsuspend_client_input_schema(), |
| 1138 | 1344 | 'output_schema' => self::get_client_output_schema(), |
| 1139 | 1345 | 'execute_callback' => array( self::class, 'execute_unsuspend_client' ), |
| 1140 | - 'permission_callback' => array( MainWP_Abilities_Util::class, 'check_manage_sites_permission' ), | |
| 1346 | + 'permission_callback' => MainWP_Abilities_Util::make_operation_permission_callback( 'mainwp/unsuspend-client-v1', array( MainWP_Abilities_Util::class, 'check_manage_sites_permission' ) ), | |
| 1141 | 1347 | 'meta' => array( |
| 1142 | 1348 | 'show_in_rest' => true, |
| 1143 | 1349 | 'annotations' => array( |
| 1144 | 1350 | 'instructions' => '', |
| @@ -1176,8 +1382,15 @@ | ||
| 1176 | 1382 | $client = MainWP_Abilities_Util::resolve_client( $input['client_id_or_email'] ); |
| 1177 | 1383 | |
| 1178 | 1384 | if ( is_wp_error( $client ) ) { |
| 1179 | 1385 | 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; | |
| 1180 | 1393 | } |
| 1181 | 1394 | |
| 1182 | 1395 | $updated = MainWP_DB_Client::instance()->update_client( |
| 1183 | 1396 | array( |