PluginProbe
MainWP Dashboard: Self-hosted WordPress Management for Agencies / 6.3
MainWP Dashboard: Self-hosted WordPress Management for Agencies v6.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-sites.php +259 -56 6.2.3 → 6.3 View file →
@@ -766,8 +766,22 @@
766 766 public static function execute_list_sites( $input ) {
767 767 $input = is_array( $input ) ? $input : array();
768 768 $db = MainWP_DB::instance();
769 769
770 + $allowed = MainWP_Abilities_Util::get_scoped_site_ids_or_all();
771 + $allowed_groups = MainWP_Abilities_Util::get_scoped_group_ids_or_all();
772 + if ( isset( $input['tag_id'] ) && 'all' !== $allowed_groups && ! in_array( (int) $input['tag_id'], $allowed_groups, true ) ) {
773 + $allowed = array();
774 + }
775 + if ( array() === $allowed ) {
776 + return array(
777 + 'items' => array(),
778 + 'page' => (int) ( $input['page'] ?? 1 ),
779 + 'per_page' => (int) ( $input['per_page'] ?? 20 ),
780 + 'total' => 0,
781 + );
782 + }
783 +
770 784 // Map ability input to DB method parameters.
771 785 $db_params = array(
772 786 'paged' => $input['page'] ?? 1,
773 787 'items_per_page' => $input['per_page'] ?? 20,
@@ -805,9 +819,13 @@
805 819 'tags' => isset( $input['tag_id'] ) ? array( (int) $input['tag_id'] ) : array(),
806 820 'client_id' => isset( $input['client_id'] ) ? (int) $input['client_id'] : 0,
807 821 's' => $input['search'] ?? '',
808 822 );
809 - $total = $db->get_websites_count_for_current_user( $count_filters );
823 + if ( 'all' !== $allowed ) {
824 + $db_params['include'] = $allowed;
825 + $count_filters['include'] = $allowed;
826 + }
827 + $total = $db->get_websites_count_for_current_user( $count_filters );
810 828
811 829 // Now get the paginated subset.
812 830 $websites = $db->get_websites_for_current_user( $db_params );
813 831
@@ -848,9 +866,9 @@
848 866 // This shouldn't happen if permission_callback passed, but handle gracefully.
849 867 return $site;
850 868 }
851 869
852 - $include_stats = $input['include_stats'] ?? false;
870 + $include_stats = MainWP_Abilities_Util::get_boolean_input( $input, 'include_stats' );
853 871
854 872 return MainWP_Abilities_Util::format_site_for_output( $site, true, $include_stats );
855 873 }
856 874
@@ -910,8 +928,14 @@
910 928 if ( is_wp_error( $all_sites ) ) {
911 929 return $all_sites;
912 930 }
913 931
932 + // API-key authentication bypasses DB site scoping.
933 + $allowed = MainWP_Abilities_Util::get_scoped_site_ids_or_all();
934 + if ( 'all' !== $allowed ) {
935 + $all_sites = array_intersect_key( $all_sites ? $all_sites : array(), array_flip( $allowed ) );
936 + }
937 +
914 938 $site_ids_or_domains = array();
915 939 if ( ! empty( $all_sites ) ) {
916 940 foreach ( $all_sites as $s ) {
917 941 $id = (int) $s->id;
@@ -1068,9 +1092,9 @@
1068 1092 $plugin_updates = array();
1069 1093 }
1070 1094
1071 1095 $status_filter = $input['status'] ?? 'all';
1072 - $has_update_filter = $input['has_update'] ?? null;
1096 + $has_update_filter = MainWP_Abilities_Util::get_boolean_input( $input, 'has_update' );
1073 1097
1074 1098 $plugins = array();
1075 1099 foreach ( $plugins_data as $key => $plugin ) {
1076 1100 // Support both formats:
@@ -1095,9 +1119,9 @@
1095 1119
1096 1120 $has_update = isset( $plugin_updates[ $slug ] );
1097 1121
1098 1122 // Apply has_update filter if specified.
1099 - if ( null !== $has_update_filter && $has_update_filter && ! $has_update ) {
1123 + if ( $has_update_filter && ! $has_update ) {
1100 1124 continue;
1101 1125 }
1102 1126
1103 1127 // Extract update version from plugin_upgrades structure.
@@ -1177,9 +1201,9 @@
1177 1201 }
1178 1202 }
1179 1203
1180 1204 $status_filter = $input['status'] ?? 'all';
1181 - $has_update_filter = $input['has_update'] ?? null;
1205 + $has_update_filter = MainWP_Abilities_Util::get_boolean_input( $input, 'has_update' );
1182 1206
1183 1207 $themes = array();
1184 1208 foreach ( $themes_data as $key => $theme ) {
1185 1209 // Support both formats:
@@ -1204,9 +1228,9 @@
1204 1228
1205 1229 $has_update = isset( $theme_updates[ $slug ] );
1206 1230
1207 1231 // Apply has_update filter if specified.
1208 - if ( null !== $has_update_filter && $has_update_filter && ! $has_update ) {
1232 + if ( $has_update_filter && ! $has_update ) {
1209 1233 continue;
1210 1234 }
1211 1235
1212 1236 // Extract update version from theme_upgrades structure.
@@ -1253,9 +1277,9 @@
1253 1277 'category' => 'mainwp-sites',
1254 1278 'input_schema' => self::get_add_site_input_schema(),
1255 1279 'output_schema' => self::get_site_output_schema(),
1256 1280 'execute_callback' => array( self::class, 'execute_add_site' ),
1257 - 'permission_callback' => array( MainWP_Abilities_Util::class, 'check_manage_sites_permission' ),
1281 + 'permission_callback' => MainWP_Abilities_Util::make_operation_permission_callback( 'mainwp/add-site-v1', array( MainWP_Abilities_Util::class, 'check_manage_sites_permission' ) ),
1258 1282 'meta' => array(
1259 1283 'show_in_rest' => true,
1260 1284 'annotations' => array(
1261 1285 'instructions' => '',
@@ -1363,8 +1387,37 @@
1363 1387 array( 'status' => 400 )
1364 1388 );
1365 1389 }
1366 1390
1391 + // Reject inaccessible relations before connecting the site, so a
1392 + // denied tag or client assignment cannot leave a new site behind.
1393 + $validated_tag_ids = array();
1394 + if ( ! empty( $input['tag_ids'] ) && is_array( $input['tag_ids'] ) ) {
1395 + foreach ( array_values( array_unique( array_map( 'intval', $input['tag_ids'] ) ) ) as $tag_id ) {
1396 + $tag = MainWP_Abilities_Util::resolve_tag( $tag_id );
1397 + if ( is_wp_error( $tag ) ) {
1398 + return $tag;
1399 + }
1400 + $tag_access = MainWP_Abilities_Util::check_tag_access( $tag, $input );
1401 + if ( is_wp_error( $tag_access ) ) {
1402 + return $tag_access;
1403 + }
1404 + $validated_tag_ids[] = $tag_id;
1405 + }
1406 + }
1407 +
1408 + $client_id = ! empty( $input['client_id'] ) ? (int) $input['client_id'] : 0;
1409 + if ( $client_id > 0 ) {
1410 + $client = MainWP_Abilities_Util::resolve_client( $client_id );
1411 + if ( is_wp_error( $client ) ) {
1412 + return $client;
1413 + }
1414 + $client_access = MainWP_Abilities_Util::check_client_access( $client, $input );
1415 + if ( is_wp_error( $client_access ) ) {
1416 + return $client_access;
1417 + }
1418 + }
1419 +
1367 1420 // Normalize URL for consistent storage and duplicate detection.
1368 1421 // This lowercases the host (per RFC 4343) and ensures trailing slash.
1369 1422 $url = MainWP_Abilities_Util::normalize_site_url( $url );
1370 1423
@@ -1436,18 +1489,14 @@
1436 1489 array( 'status' => 500 )
1437 1490 );
1438 1491 }
1439 1492
1440 - // Assign tags if provided.
1441 - if ( ! empty( $input['tag_ids'] ) && is_array( $input['tag_ids'] ) ) {
1442 - foreach ( $input['tag_ids'] as $tag_id ) {
1443 - MainWP_DB_Common::instance()->update_group_site( (int) $tag_id, $site_id );
1444 - }
1493 + foreach ( $validated_tag_ids as $tag_id ) {
1494 + MainWP_DB_Common::instance()->update_group_site( $tag_id, $site_id );
1445 1495 }
1446 1496
1447 - // Assign client if provided.
1448 - if ( ! empty( $input['client_id'] ) ) {
1449 - MainWP_DB::instance()->update_website_values( $site_id, array( 'client_id' => (int) $input['client_id'] ) );
1497 + if ( $client_id > 0 ) {
1498 + MainWP_DB::instance()->update_website_values( $site_id, array( 'client_id' => $client_id ) );
1450 1499 }
1451 1500
1452 1501 // Re-fetch site to get updated data.
1453 1502 $site = MainWP_DB::instance()->get_website_by_id( $site_id );
@@ -1469,9 +1518,9 @@
1469 1518 'category' => 'mainwp-sites',
1470 1519 'input_schema' => self::get_update_site_input_schema(),
1471 1520 'output_schema' => self::get_site_output_schema(),
1472 1521 'execute_callback' => array( self::class, 'execute_update_site' ),
1473 - 'permission_callback' => array( MainWP_Abilities_Util::class, 'check_manage_sites_permission' ),
1522 + 'permission_callback' => MainWP_Abilities_Util::make_operation_permission_callback( 'mainwp/update-site-v1', array( MainWP_Abilities_Util::class, 'check_manage_sites_permission' ) ),
1474 1523 'meta' => array(
1475 1524 'show_in_rest' => true,
1476 1525 'annotations' => array(
1477 1526 'instructions' => '',
@@ -1561,13 +1610,53 @@
1561 1610 if ( is_wp_error( $site ) ) {
1562 1611 return $site;
1563 1612 }
1564 1613
1565 - $access_check = MainWP_Abilities_Util::check_site_access( $site );
1614 + $access_check = MainWP_Abilities_Util::check_site_access( $site, $input );
1566 1615 if ( is_wp_error( $access_check ) ) {
1567 1616 return $access_check;
1568 1617 }
1569 1618
1619 + // Check relations before any write: attaching a site to an inaccessible
1620 + // client would itself create the overlap used to grant client access.
1621 + $validated_tag_ids = null;
1622 + if ( isset( $input['tag_ids'] ) && is_array( $input['tag_ids'] ) ) {
1623 + $validated_tag_ids = array();
1624 + foreach ( array_values( array_unique( array_map( 'intval', $input['tag_ids'] ) ) ) as $tag_id ) {
1625 + $tag = MainWP_Abilities_Util::resolve_tag( $tag_id );
1626 + if ( is_wp_error( $tag ) ) {
1627 + return $tag;
1628 + }
1629 + $tag_access = MainWP_Abilities_Util::check_tag_access( $tag, $input );
1630 + if ( is_wp_error( $tag_access ) ) {
1631 + return $tag_access;
1632 + }
1633 + $validated_tag_ids[] = $tag_id;
1634 + }
1635 + }
1636 +
1637 + $new_client_id = null;
1638 + if ( isset( $input['client_id'] ) ) {
1639 + $new_client_id = (int) $input['client_id'];
1640 + if ( $new_client_id < 0 ) {
1641 + return new \WP_Error(
1642 + 'mainwp_invalid_input',
1643 + __( 'client_id must be 0 to unassign or a positive client ID.', 'mainwp' ),
1644 + array( 'status' => 400 )
1645 + );
1646 + }
1647 + if ( $new_client_id > 0 ) {
1648 + $client = MainWP_Abilities_Util::resolve_client( $new_client_id );
1649 + if ( is_wp_error( $client ) ) {
1650 + return $client;
1651 + }
1652 + $client_access = MainWP_Abilities_Util::check_client_access( $client, $input );
1653 + if ( is_wp_error( $client_access ) ) {
1654 + return $client_access;
1655 + }
1656 + }
1657 + }
1658 +
1570 1659 $update_data = array();
1571 1660
1572 1661 if ( isset( $input['name'] ) ) {
1573 1662 $update_data['name'] = sanitize_text_field( $input['name'] );
@@ -1601,9 +1690,24 @@
1601 1690 MainWP_Site_Url_Corrector::after_user_set_url( $site, $update_data['url'] );
1602 1691 }
1603 1692 }
1604 1693
1605 - if ( isset( $input['tag_ids'] ) && is_array( $input['tag_ids'] ) ) {
1694 + if ( null !== $validated_tag_ids ) {
1695 + // A groups-restricted caller only manages their slice of the tag
1696 + // set: associations outside the allowed groups are invisible to
1697 + // them and must survive the replace.
1698 + $allowed_groups = MainWP_Abilities_Util::get_allowed_group_ids();
1699 + $preserved = array();
1700 + if ( 'all' !== $allowed_groups ) {
1701 + $existing_groups = MainWP_DB_Common::instance()->get_groups_by_website_id( $site->id );
1702 + foreach ( (array) $existing_groups as $existing_group ) {
1703 + $existing_id = (int) $existing_group->id;
1704 + if ( ! in_array( $existing_id, $allowed_groups, true ) ) {
1705 + $preserved[] = $existing_id;
1706 + }
1707 + }
1708 + }
1709 +
1606 1710 global $wpdb;
1607 1711 // Clear existing tag associations for this site.
1608 1712 //phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.DirectQuery -- for testing results.
1609 1713 $wpdb->query(
@@ -1612,15 +1716,15 @@
1612 1716 $site->id
1613 1717 )
1614 1718 );
1615 1719 // Add new tag associations.
1616 - foreach ( $input['tag_ids'] as $tag_id ) {
1720 + foreach ( array_unique( array_merge( $preserved, $validated_tag_ids ) ) as $tag_id ) {
1617 1721 MainWP_DB_Common::instance()->update_group_site( (int) $tag_id, $site->id );
1618 1722 }
1619 1723 }
1620 1724
1621 - if ( isset( $input['client_id'] ) ) {
1622 - MainWP_DB::instance()->update_website_values( $site->id, array( 'client_id' => (int) $input['client_id'] ) );
1725 + if ( null !== $new_client_id ) {
1726 + MainWP_DB::instance()->update_website_values( $site->id, array( 'client_id' => $new_client_id ) );
1623 1727 }
1624 1728
1625 1729 $site = MainWP_DB::instance()->get_website_by_id( $site->id );
1626 1730
@@ -1641,9 +1745,9 @@
1641 1745 'category' => 'mainwp-sites',
1642 1746 'input_schema' => self::get_delete_site_input_schema(),
1643 1747 'output_schema' => self::get_delete_site_output_schema(),
1644 1748 'execute_callback' => array( self::class, 'execute_delete_site' ),
1645 - 'permission_callback' => array( MainWP_Abilities_Util::class, 'check_manage_sites_permission' ),
1749 + 'permission_callback' => MainWP_Abilities_Util::make_operation_permission_callback( 'mainwp/delete-site-v1', array( MainWP_Abilities_Util::class, 'check_manage_sites_permission' ) ),
1646 1750 'meta' => array(
1647 1751 'show_in_rest' => true,
1648 1752 'annotations' => array(
1649 1753 'instructions' => 'Destructive operation - requires confirm:true or dry_run:true. Only call when user explicitly requests deletion.',
@@ -1726,10 +1830,10 @@
1726 1830 */
1727 1831 public static function execute_delete_site( $input ) {
1728 1832 $input = MainWP_Abilities_Util::normalize_input( $input );
1729 1833
1730 - $dry_run = ! empty( $input['dry_run'] );
1731 - $confirm = ! empty( $input['confirm'] );
1834 + $dry_run = MainWP_Abilities_Util::get_boolean_input( $input, 'dry_run' );
1835 + $confirm = MainWP_Abilities_Util::get_boolean_input( $input, 'confirm' );
1732 1836
1733 1837 if ( $dry_run && $confirm ) {
1734 1838 return new \WP_Error(
1735 1839 'mainwp_invalid_input',
@@ -1742,9 +1846,9 @@
1742 1846 if ( is_wp_error( $site ) ) {
1743 1847 return $site;
1744 1848 }
1745 1849
1746 - $access_check = MainWP_Abilities_Util::check_site_access( $site );
1850 + $access_check = MainWP_Abilities_Util::check_site_access( $site, $input );
1747 1851 if ( is_wp_error( $access_check ) ) {
1748 1852 return $access_check;
1749 1853 }
1750 1854
@@ -1780,8 +1884,15 @@
1780 1884 'url' => $site->url,
1781 1885 'name' => MainWP_Utility::remove_http_prefix( (string) $site->name, true ),
1782 1886 );
1783 1887
1888 + // Same favicon cleanup, and the same point relative to the DB delete,
1889 + // as MainWP_Manage_Sites_Handler::remove_website(). The file name is the
1890 + // stored favi_icon option. A missing file or a cleanup failure must not
1891 + // change this response. remove_website() itself is not called: it
1892 + // deactivates the child plugin over the network.
1893 + MainWP_Manage_Sites_Handler::delete_website_favicon( $site );
1894 +
1784 1895 $result = MainWP_DB::instance()->remove_website( $site->id );
1785 1896
1786 1897 if ( false === $result ) {
1787 1898 return new \WP_Error(
@@ -1790,8 +1901,40 @@
1790 1901 array( 'status' => 500 )
1791 1902 );
1792 1903 }
1793 1904
1905 + // Same hooks, argument, and order as MainWP_Manage_Sites_Handler::remove_website().
1906 + // That handler is not called here: it deactivates the child plugin over the network
1907 + // and applies can_edit_website(), which would change this ability's contract.
1908 + // Demo sites fire the same hooks (still with no remote deactivate) so extension
1909 + // rows are removed. Hooks run only after a successful delete so a failed removal
1910 + // does not tell extensions to drop their per-site rows.
1911 + /**
1912 + * Fires after a child site has been removed from the MainWP Dashboard.
1913 + *
1914 + * @param object $website Object containing child site data.
1915 + *
1916 + * @since 3.4
1917 + */
1918 + try {
1919 + do_action( 'mainwp_delete_site', $site );
1920 + } catch ( \Throwable $e ) {
1921 + MainWP_Logger::instance()->debug( 'mainwp_delete_site listener failed: ' . $e->getMessage() );
1922 + }
1923 +
1924 + /**
1925 + * Fires immediately after a website is deleted.
1926 + *
1927 + * @since 4.5.1.1
1928 + *
1929 + * @param object $website Website data.
1930 + */
1931 + try {
1932 + do_action( 'mainwp_site_deleted', $site );
1933 + } catch ( \Throwable $e ) {
1934 + MainWP_Logger::instance()->debug( 'mainwp_site_deleted listener failed: ' . $e->getMessage() );
1935 + }
1936 +
1794 1937 return array(
1795 1938 'dry_run' => false,
1796 1939 'deleted' => true,
1797 1940 'site' => $site_info,
@@ -1814,9 +1957,9 @@
1814 1957 'category' => 'mainwp-sites',
1815 1958 'input_schema' => self::get_single_site_input_schema(),
1816 1959 'output_schema' => self::get_reconnect_site_output_schema(),
1817 1960 'execute_callback' => array( self::class, 'execute_reconnect_site' ),
1818 - 'permission_callback' => array( MainWP_Abilities_Util::class, 'check_manage_sites_permission' ),
1961 + 'permission_callback' => MainWP_Abilities_Util::make_operation_permission_callback( 'mainwp/reconnect-site-v1', array( MainWP_Abilities_Util::class, 'check_manage_sites_permission' ) ),
1819 1962 'meta' => array(
1820 1963 'show_in_rest' => true,
1821 1964 'annotations' => array(
1822 1965 'instructions' => '',
@@ -1881,9 +2024,9 @@
1881 2024 if ( is_wp_error( $site ) ) {
1882 2025 return $site;
1883 2026 }
1884 2027
1885 - $access_check = MainWP_Abilities_Util::check_site_access( $site );
2028 + $access_check = MainWP_Abilities_Util::check_site_access( $site, $input );
1886 2029 if ( is_wp_error( $access_check ) ) {
1887 2030 return $access_check;
1888 2031 }
1889 2032
@@ -1928,9 +2071,9 @@
1928 2071 'category' => 'mainwp-sites',
1929 2072 'input_schema' => self::get_single_site_input_schema(),
1930 2073 'output_schema' => self::get_disconnect_site_output_schema(),
1931 2074 'execute_callback' => array( self::class, 'execute_disconnect_site' ),
1932 - 'permission_callback' => array( MainWP_Abilities_Util::class, 'check_manage_sites_permission' ),
2075 + 'permission_callback' => MainWP_Abilities_Util::make_operation_permission_callback( 'mainwp/disconnect-site-v1', array( MainWP_Abilities_Util::class, 'check_manage_sites_permission' ) ),
1933 2076 'meta' => array(
1934 2077 'show_in_rest' => true,
1935 2078 'annotations' => array(
1936 2079 'instructions' => '',
@@ -1977,9 +2120,9 @@
1977 2120 if ( is_wp_error( $site ) ) {
1978 2121 return $site;
1979 2122 }
1980 2123
1981 - $access_check = MainWP_Abilities_Util::check_site_access( $site );
2124 + $access_check = MainWP_Abilities_Util::check_site_access( $site, $input );
1982 2125 if ( is_wp_error( $access_check ) ) {
1983 2126 return $access_check;
1984 2127 }
1985 2128
@@ -2007,9 +2150,9 @@
2007 2150 'category' => 'mainwp-sites',
2008 2151 'input_schema' => self::get_single_site_input_schema(),
2009 2152 'output_schema' => self::get_suspend_site_output_schema(),
2010 2153 'execute_callback' => array( self::class, 'execute_suspend_site' ),
2011 - 'permission_callback' => array( MainWP_Abilities_Util::class, 'check_manage_sites_permission' ),
2154 + 'permission_callback' => MainWP_Abilities_Util::make_operation_permission_callback( 'mainwp/suspend-site-v1', array( MainWP_Abilities_Util::class, 'check_manage_sites_permission' ) ),
2012 2155 'meta' => array(
2013 2156 'show_in_rest' => true,
2014 2157 'annotations' => array(
2015 2158 'instructions' => '',
@@ -2056,9 +2199,9 @@
2056 2199 if ( is_wp_error( $site ) ) {
2057 2200 return $site;
2058 2201 }
2059 2202
2060 - $access_check = MainWP_Abilities_Util::check_site_access( $site );
2203 + $access_check = MainWP_Abilities_Util::check_site_access( $site, $input );
2061 2204 if ( is_wp_error( $access_check ) ) {
2062 2205 return $access_check;
2063 2206 }
2064 2207
@@ -2088,9 +2231,9 @@
2088 2231 'category' => 'mainwp-sites',
2089 2232 'input_schema' => self::get_single_site_input_schema(),
2090 2233 'output_schema' => self::get_unsuspend_site_output_schema(),
2091 2234 'execute_callback' => array( self::class, 'execute_unsuspend_site' ),
2092 - 'permission_callback' => array( MainWP_Abilities_Util::class, 'check_manage_sites_permission' ),
2235 + 'permission_callback' => MainWP_Abilities_Util::make_operation_permission_callback( 'mainwp/unsuspend-site-v1', array( MainWP_Abilities_Util::class, 'check_manage_sites_permission' ) ),
2093 2236 'meta' => array(
2094 2237 'show_in_rest' => true,
2095 2238 'annotations' => array(
2096 2239 'instructions' => '',
@@ -2137,9 +2280,9 @@
2137 2280 if ( is_wp_error( $site ) ) {
2138 2281 return $site;
2139 2282 }
2140 2283
2141 - $access_check = MainWP_Abilities_Util::check_site_access( $site );
2284 + $access_check = MainWP_Abilities_Util::check_site_access( $site, $input );
2142 2285 if ( is_wp_error( $access_check ) ) {
2143 2286 return $access_check;
2144 2287 }
2145 2288
@@ -2169,9 +2312,9 @@
2169 2312 'category' => 'mainwp-sites',
2170 2313 'input_schema' => self::get_single_site_input_schema(),
2171 2314 'output_schema' => self::get_check_site_output_schema(),
2172 2315 'execute_callback' => array( self::class, 'execute_check_site' ),
2173 - 'permission_callback' => array( MainWP_Abilities_Util::class, 'check_rest_api_permission' ),
2316 + 'permission_callback' => MainWP_Abilities_Util::make_operation_permission_callback( 'mainwp/check-site-v1', array( MainWP_Abilities_Util::class, 'check_rest_api_permission' ) ),
2174 2317 'meta' => array(
2175 2318 'show_in_rest' => true,
2176 2319 'annotations' => array(
2177 2320 'instructions' => '',
@@ -2235,9 +2378,9 @@
2235 2378 if ( is_wp_error( $site ) ) {
2236 2379 return $site;
2237 2380 }
2238 2381
2239 - $access_check = MainWP_Abilities_Util::check_site_access( $site );
2382 + $access_check = MainWP_Abilities_Util::check_site_access( $site, $input );
2240 2383 if ( is_wp_error( $access_check ) ) {
2241 2384 return $access_check;
2242 2385 }
2243 2386
@@ -2309,9 +2452,9 @@
2309 2452 'category' => 'mainwp-sites',
2310 2453 'input_schema' => self::get_activate_site_plugins_input_schema(),
2311 2454 'output_schema' => self::get_activate_plugins_output_schema(),
2312 2455 'execute_callback' => array( self::class, 'execute_activate_site_plugins' ),
2313 - 'permission_callback' => array( MainWP_Abilities_Util::class, 'check_manage_sites_permission' ),
2456 + 'permission_callback' => MainWP_Abilities_Util::make_operation_permission_callback( 'mainwp/activate-site-plugins-v1', array( MainWP_Abilities_Util::class, 'check_manage_sites_permission' ) ),
2314 2457 'meta' => array(
2315 2458 'show_in_rest' => true,
2316 2459 'annotations' => array(
2317 2460 'instructions' => '',
@@ -2421,9 +2564,9 @@
2421 2564 if ( is_wp_error( $site ) ) {
2422 2565 return $site;
2423 2566 }
2424 2567
2425 - $access_check = MainWP_Abilities_Util::check_site_access( $site );
2568 + $access_check = MainWP_Abilities_Util::check_site_access( $site, $input );
2426 2569 if ( is_wp_error( $access_check ) ) {
2427 2570 return $access_check;
2428 2571 }
2429 2572
@@ -2493,9 +2636,9 @@
2493 2636 'category' => 'mainwp-sites',
2494 2637 'input_schema' => self::get_deactivate_site_plugins_input_schema(),
2495 2638 'output_schema' => self::get_deactivate_plugins_output_schema(),
2496 2639 'execute_callback' => array( self::class, 'execute_deactivate_site_plugins' ),
2497 - 'permission_callback' => array( MainWP_Abilities_Util::class, 'check_manage_sites_permission' ),
2640 + 'permission_callback' => MainWP_Abilities_Util::make_operation_permission_callback( 'mainwp/deactivate-site-plugins-v1', array( MainWP_Abilities_Util::class, 'check_manage_sites_permission' ) ),
2498 2641 'meta' => array(
2499 2642 'show_in_rest' => true,
2500 2643 'annotations' => array(
2501 2644 'instructions' => '',
@@ -2545,9 +2688,9 @@
2545 2688 if ( is_wp_error( $site ) ) {
2546 2689 return $site;
2547 2690 }
2548 2691
2549 - $access_check = MainWP_Abilities_Util::check_site_access( $site );
2692 + $access_check = MainWP_Abilities_Util::check_site_access( $site, $input );
2550 2693 if ( is_wp_error( $access_check ) ) {
2551 2694 return $access_check;
2552 2695 }
2553 2696
@@ -2617,9 +2760,9 @@
2617 2760 'category' => 'mainwp-sites',
2618 2761 'input_schema' => self::get_delete_site_plugins_input_schema(),
2619 2762 'output_schema' => self::get_delete_plugins_output_schema(),
2620 2763 'execute_callback' => array( self::class, 'execute_delete_site_plugins' ),
2621 - 'permission_callback' => array( MainWP_Abilities_Util::class, 'check_manage_sites_permission' ),
2764 + 'permission_callback' => MainWP_Abilities_Util::make_operation_permission_callback( 'mainwp/delete-site-plugins-v1', array( MainWP_Abilities_Util::class, 'check_manage_sites_permission' ) ),
2622 2765 'meta' => array(
2623 2766 'show_in_rest' => true,
2624 2767 'annotations' => array(
2625 2768 'instructions' => 'Destructive operation - requires confirm:true or dry_run:true. Only call when user explicitly requests deletion. Permanently removes plugins from the child site.',
@@ -2721,10 +2864,10 @@
2721 2864 */
2722 2865 private static function execute_delete_remote_items( array $input, array $adapter ) {
2723 2866 $input = MainWP_Abilities_Util::normalize_input( $input );
2724 2867
2725 - $dry_run = ! empty( $input['dry_run'] );
2726 - $confirm = ! empty( $input['confirm'] );
2868 + $dry_run = MainWP_Abilities_Util::get_boolean_input( $input, 'dry_run' );
2869 + $confirm = MainWP_Abilities_Util::get_boolean_input( $input, 'confirm' );
2727 2870
2728 2871 if ( $dry_run && $confirm ) {
2729 2872 return new \WP_Error(
2730 2873 'mainwp_invalid_input',
@@ -2737,9 +2880,9 @@
2737 2880 if ( is_wp_error( $site ) ) {
2738 2881 return $site;
2739 2882 }
2740 2883
2741 - $access_check = MainWP_Abilities_Util::check_site_access( $site );
2884 + $access_check = MainWP_Abilities_Util::check_site_access( $site, $input );
2742 2885 if ( is_wp_error( $access_check ) ) {
2743 2886 return $access_check;
2744 2887 }
2745 2888
@@ -2989,9 +3132,9 @@
2989 3132 if ( is_wp_error( $site ) ) {
2990 3133 return $site;
2991 3134 }
2992 3135
2993 - $access_check = MainWP_Abilities_Util::check_site_access( $site );
3136 + $access_check = MainWP_Abilities_Util::check_site_access( $site, $input );
2994 3137 if ( is_wp_error( $access_check ) ) {
2995 3138 return $access_check;
2996 3139 }
2997 3140
@@ -3049,9 +3192,9 @@
3049 3192 'category' => 'mainwp-sites',
3050 3193 'input_schema' => self::get_activate_site_theme_input_schema(),
3051 3194 'output_schema' => self::get_activate_theme_output_schema(),
3052 3195 'execute_callback' => array( self::class, 'execute_activate_site_theme' ),
3053 - 'permission_callback' => array( MainWP_Abilities_Util::class, 'check_manage_sites_permission' ),
3196 + 'permission_callback' => MainWP_Abilities_Util::make_operation_permission_callback( 'mainwp/activate-site-theme-v1', array( MainWP_Abilities_Util::class, 'check_manage_sites_permission' ) ),
3054 3197 'meta' => array(
3055 3198 'show_in_rest' => true,
3056 3199 'annotations' => array(
3057 3200 'instructions' => '',
@@ -3114,9 +3257,9 @@
3114 3257 if ( is_wp_error( $site ) ) {
3115 3258 return $site;
3116 3259 }
3117 3260
3118 - $access_check = MainWP_Abilities_Util::check_site_access( $site );
3261 + $access_check = MainWP_Abilities_Util::check_site_access( $site, $input );
3119 3262 if ( is_wp_error( $access_check ) ) {
3120 3263 return $access_check;
3121 3264 }
3122 3265
@@ -3174,9 +3317,9 @@
3174 3317 'category' => 'mainwp-sites',
3175 3318 'input_schema' => self::get_delete_site_themes_input_schema(),
3176 3319 'output_schema' => self::get_delete_themes_output_schema(),
3177 3320 'execute_callback' => array( self::class, 'execute_delete_site_themes' ),
3178 - 'permission_callback' => array( MainWP_Abilities_Util::class, 'check_manage_sites_permission' ),
3321 + 'permission_callback' => MainWP_Abilities_Util::make_operation_permission_callback( 'mainwp/delete-site-themes-v1', array( MainWP_Abilities_Util::class, 'check_manage_sites_permission' ) ),
3179 3322 'meta' => array(
3180 3323 'show_in_rest' => true,
3181 3324 'annotations' => array(
3182 3325 'instructions' => 'Destructive operation - requires confirm:true or dry_run:true. Only call when user explicitly requests deletion. Permanently removes themes from the child site.',
@@ -3346,9 +3489,9 @@
3346 3489 if ( is_wp_error( $site ) ) {
3347 3490 return $site;
3348 3491 }
3349 3492
3350 - $access_check = MainWP_Abilities_Util::check_site_access( $site );
3493 + $access_check = MainWP_Abilities_Util::check_site_access( $site, $input );
3351 3494 if ( is_wp_error( $access_check ) ) {
3352 3495 return $access_check;
3353 3496 }
3354 3497
@@ -3450,9 +3593,9 @@
3450 3593 if ( is_wp_error( $site ) ) {
3451 3594 return $site;
3452 3595 }
3453 3596
3454 - $access_check = MainWP_Abilities_Util::check_site_access( $site );
3597 + $access_check = MainWP_Abilities_Util::check_site_access( $site, $input );
3455 3598 if ( is_wp_error( $access_check ) ) {
3456 3599 return $access_check;
3457 3600 }
3458 3601
@@ -3580,9 +3723,9 @@
3580 3723 if ( is_wp_error( $site ) ) {
3581 3724 return $site;
3582 3725 }
3583 3726
3584 - $access_check = MainWP_Abilities_Util::check_site_access( $site );
3727 + $access_check = MainWP_Abilities_Util::check_site_access( $site, $input );
3585 3728 if ( is_wp_error( $access_check ) ) {
3586 3729 return $access_check;
3587 3730 }
3588 3731
@@ -3714,9 +3857,9 @@
3714 3857 if ( is_wp_error( $site ) ) {
3715 3858 return $site;
3716 3859 }
3717 3860
3718 - $access_check = MainWP_Abilities_Util::check_site_access( $site );
3861 + $access_check = MainWP_Abilities_Util::check_site_access( $site, $input );
3719 3862 if ( is_wp_error( $access_check ) ) {
3720 3863 return $access_check;
3721 3864 }
3722 3865
@@ -3821,9 +3964,9 @@
3821 3964 if ( is_wp_error( $site ) ) {
3822 3965 return $site;
3823 3966 }
3824 3967
3825 - $access_check = MainWP_Abilities_Util::check_site_access( $site );
3968 + $access_check = MainWP_Abilities_Util::check_site_access( $site, $input );
3826 3969 if ( is_wp_error( $access_check ) ) {
3827 3970 return $access_check;
3828 3971 }
3829 3972
@@ -3924,9 +4067,26 @@
3924 4067 */
3925 4068 public static function execute_count_sites( $input ) {
3926 4069 $input = MainWP_Abilities_Util::normalize_input( $input );
3927 4070
4071 + $allowed = MainWP_Abilities_Util::get_scoped_site_ids_or_all();
4072 + if ( ! empty( $input['tag_ids'] ) && is_array( $input['tag_ids'] ) ) {
4073 + $allowed_groups = MainWP_Abilities_Util::get_scoped_group_ids_or_all();
4074 + if ( 'all' !== $allowed_groups ) {
4075 + $input['tag_ids'] = array_values( array_intersect( array_map( 'intval', $input['tag_ids'] ), $allowed_groups ) );
4076 + if ( empty( $input['tag_ids'] ) ) {
4077 + $allowed = array();
4078 + }
4079 + }
4080 + }
4081 + if ( array() === $allowed ) {
4082 + return array( 'total' => 0 );
4083 + }
4084 +
3928 4085 $filters = array();
4086 + if ( 'all' !== $allowed ) {
4087 + $filters['include'] = $allowed;
4088 + }
3929 4089
3930 4090 if ( ! empty( $input['status'] ) ) {
3931 4091 $filters['status'] = $input['status'];
3932 4092 }
@@ -4054,8 +4214,27 @@
4054 4214
4055 4215 $page = max( 1, (int) $input['page'] );
4056 4216 $per_page = max( 1, min( 100, (int) $input['per_page'] ) );
4057 4217
4218 + $allowed = MainWP_Abilities_Util::get_scoped_site_ids_or_all();
4219 + if ( ! empty( $input['tag_ids'] ) && is_array( $input['tag_ids'] ) ) {
4220 + $allowed_groups = MainWP_Abilities_Util::get_scoped_group_ids_or_all();
4221 + if ( 'all' !== $allowed_groups ) {
4222 + $input['tag_ids'] = array_values( array_intersect( array_map( 'intval', $input['tag_ids'] ), $allowed_groups ) );
4223 + if ( empty( $input['tag_ids'] ) ) {
4224 + $allowed = array();
4225 + }
4226 + }
4227 + }
4228 + if ( array() === $allowed ) {
4229 + return array(
4230 + 'items' => array(),
4231 + 'page' => $page,
4232 + 'per_page' => $per_page,
4233 + 'total' => 0,
4234 + );
4235 + }
4236 +
4058 4237 $filters = array(
4059 4238 'offset' => ( $page - 1 ) * $per_page,
4060 4239 'rowcount' => $per_page,
4061 4240 'selectgroups' => false,
@@ -4060,8 +4239,12 @@
4060 4239 'rowcount' => $per_page,
4061 4240 'selectgroups' => false,
4062 4241 );
4063 4242
4243 + if ( 'all' !== $allowed ) {
4244 + $filters['include'] = $allowed;
4245 + }
4246 +
4064 4247 if ( ! empty( $input['status'] ) ) {
4065 4248 $filters['status'] = $input['status'];
4066 4249 }
4067 4250
@@ -4120,9 +4303,9 @@
4120 4303 'category' => 'mainwp-sites',
4121 4304 'input_schema' => self::get_batch_sites_input_schema(),
4122 4305 'output_schema' => self::get_batch_operation_output_schema(),
4123 4306 'execute_callback' => array( self::class, 'execute_reconnect_sites' ),
4124 - 'permission_callback' => array( MainWP_Abilities_Util::class, 'check_manage_sites_permission' ),
4307 + 'permission_callback' => MainWP_Abilities_Util::make_operation_permission_callback( 'mainwp/reconnect-sites-v1', array( MainWP_Abilities_Util::class, 'check_manage_sites_permission' ) ),
4125 4308 'meta' => array(
4126 4309 'show_in_rest' => true,
4127 4310 'annotations' => array(
4128 4311 'instructions' => 'Pass site_ids_or_domains with specific IDs, or empty array for all applicable sites. Operations with >200 sites are automatically queued for background processing.',
@@ -4228,8 +4411,13 @@
4228 4411 if ( is_wp_error( $all_sites ) ) {
4229 4412 return $all_sites;
4230 4413 }
4231 4414
4415 + $allowed = MainWP_Abilities_Util::get_scoped_site_ids_or_all();
4416 + if ( 'all' !== $allowed ) {
4417 + $all_sites = array_intersect_key( $all_sites ? $all_sites : array(), array_flip( $allowed ) );
4418 + }
4419 +
4232 4420 $identifiers = array_map(
4233 4421 function ( $s ) {
4234 4422 return (int) $s->id;
4235 4423 },
@@ -4317,9 +4505,9 @@
4317 4505 'category' => 'mainwp-sites',
4318 4506 'input_schema' => self::get_batch_sites_input_schema(),
4319 4507 'output_schema' => self::get_batch_operation_output_schema(),
4320 4508 'execute_callback' => array( self::class, 'execute_disconnect_sites' ),
4321 - 'permission_callback' => array( MainWP_Abilities_Util::class, 'check_manage_sites_permission' ),
4509 + 'permission_callback' => MainWP_Abilities_Util::make_operation_permission_callback( 'mainwp/disconnect-sites-v1', array( MainWP_Abilities_Util::class, 'check_manage_sites_permission' ) ),
4322 4510 'meta' => array(
4323 4511 'show_in_rest' => true,
4324 4512 'annotations' => array(
4325 4513 'instructions' => 'Pass site_ids_or_domains with specific IDs, or empty array for all applicable sites. Operations with >200 sites are automatically queued for background processing.',
@@ -4362,8 +4550,13 @@
4362 4550 if ( is_wp_error( $all_sites ) ) {
4363 4551 return $all_sites;
4364 4552 }
4365 4553
4554 + $allowed = MainWP_Abilities_Util::get_scoped_site_ids_or_all();
4555 + if ( 'all' !== $allowed ) {
4556 + $all_sites = array_intersect_key( $all_sites ? $all_sites : array(), array_flip( $allowed ) );
4557 + }
4558 +
4366 4559 $identifiers = array_map(
4367 4560 function ( $s ) {
4368 4561 return (int) $s->id;
4369 4562 },
@@ -4433,9 +4626,9 @@
4433 4626 'category' => 'mainwp-sites',
4434 4627 'input_schema' => self::get_batch_sites_input_schema(),
4435 4628 'output_schema' => self::get_batch_operation_output_schema(),
4436 4629 'execute_callback' => array( self::class, 'execute_check_sites' ),
4437 - 'permission_callback' => array( MainWP_Abilities_Util::class, 'check_rest_api_permission' ),
4630 + 'permission_callback' => MainWP_Abilities_Util::make_operation_permission_callback( 'mainwp/check-sites-v1', array( MainWP_Abilities_Util::class, 'check_rest_api_permission' ) ),
4438 4631 'meta' => array(
4439 4632 'show_in_rest' => true,
4440 4633 'annotations' => array(
4441 4634 'instructions' => 'Pass site_ids_or_domains with specific IDs, or empty array for all applicable sites. Operations with >200 sites are automatically queued for background processing.',
@@ -4478,8 +4671,13 @@
4478 4671 if ( is_wp_error( $all_sites ) ) {
4479 4672 return $all_sites;
4480 4673 }
4481 4674
4675 + $allowed = MainWP_Abilities_Util::get_scoped_site_ids_or_all();
4676 + if ( 'all' !== $allowed ) {
4677 + $all_sites = array_intersect_key( $all_sites ? $all_sites : array(), array_flip( $allowed ) );
4678 + }
4679 +
4482 4680 $identifiers = array_map(
4483 4681 function ( $s ) {
4484 4682 return (int) $s->id;
4485 4683 },
@@ -4575,9 +4773,9 @@
4575 4773 'category' => 'mainwp-sites',
4576 4774 'input_schema' => self::get_batch_sites_input_schema(),
4577 4775 'output_schema' => self::get_batch_operation_output_schema(),
4578 4776 'execute_callback' => array( self::class, 'execute_suspend_sites' ),
4579 - 'permission_callback' => array( MainWP_Abilities_Util::class, 'check_manage_sites_permission' ),
4777 + 'permission_callback' => MainWP_Abilities_Util::make_operation_permission_callback( 'mainwp/suspend-sites-v1', array( MainWP_Abilities_Util::class, 'check_manage_sites_permission' ) ),
4580 4778 'meta' => array(
4581 4779 'show_in_rest' => true,
4582 4780 'annotations' => array(
4583 4781 'instructions' => 'Pass site_ids_or_domains with specific IDs, or empty array for all applicable sites. Operations with >200 sites are automatically queued for background processing.',
@@ -4618,8 +4816,13 @@
4618 4816
4619 4817 // Handle potential errors from DB query.
4620 4818 if ( is_wp_error( $all_sites ) ) {
4621 4819 return $all_sites;
4820 + }
4821 +
4822 + $allowed = MainWP_Abilities_Util::get_scoped_site_ids_or_all();
4823 + if ( 'all' !== $allowed ) {
4824 + $all_sites = array_intersect_key( $all_sites ? $all_sites : array(), array_flip( $allowed ) );
4622 4825 }
4623 4826
4624 4827 $identifiers = array_map(
4625 4828 function ( $s ) {