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 | class/class-mainwp-post-handler.php +465 -49 6.2 → 6.3 View file →
@@ -119,9 +119,8 @@
119 119 // Page: User.
120 120 $this->add_action( 'mainwp_users_search', array( &$this, 'mainwp_users_search' ) );
121 121
122 122 $this->add_action( 'mainwp_events_notice_hide', array( &$this, 'mainwp_events_notice_hide' ) );
123 - $this->add_action( 'mainwp_showhide_sections', array( &$this, 'mainwp_showhide_sections' ) );
124 123 $this->add_action( 'mainwp_saving_status', array( &$this, 'mainwp_saving_status' ) );
125 124 $this->add_action( 'mainwp_autoupdate_and_trust_child', array( &$this, 'mainwp_autoupdate_and_trust_child' ) );
126 125 $this->add_action( 'mainwp_installation_warning_hide', array( &$this, 'mainwp_installation_warning_hide' ) );
127 126 $this->add_action( 'mainwp_force_destroy_sessions', array( &$this, 'mainwp_force_destroy_sessions' ) );
@@ -721,8 +720,15 @@
721 720 *
722 721 * @uses \MainWP\Dashboard\MainWP_Utility::update_option()
723 722 */
724 723 public function ajax_mainwp_save_settings() {
724 + // Authorize before secure_request records the duplicate-request marker.
725 + if ( ! \mainwp_current_user_can( 'dashboard', 'manage_dashboard_settings' ) ) {
726 + $ret = array( 'success' => false );
727 + $ret['result'] = esc_html__( 'Error - insufficient permissions!', 'mainwp' );
728 + $ret['error'] = $ret['result'];
729 + MainWP_Client::exit_json( $ret );
730 + }
725 731 $this->secure_request( 'mainwp_save_settings' );
726 732 // phpcs:disable WordPress.Security.NonceVerification,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
727 733 $name = isset( $_POST['name'] ) ? sanitize_text_field( wp_unslash( $_POST['name'] ) ) : '';
728 734 if ( ! empty( $name ) ) {
@@ -1020,8 +1026,13 @@
1020 1026 $ret = array( 'success' => false );
1021 1027 // phpcs:disable WordPress.Security.NonceVerification,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1022 1028 $client_id = isset( $_POST['clientid'] ) ? intval( $_POST['clientid'] ) : 0;
1023 1029 // phpcs:enable WordPress.Security.NonceVerification,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1030 + if ( ! \mainwp_current_user_can( 'dashboard', 'manage_clients' ) || ! MainWP_Client::has_client_access( $client_id, true ) || ( $client_id && ! MainWP_DB_Client::instance()->get_wp_client_by( 'client_id', $client_id ) ) ) {
1031 + $ret['result'] = esc_html__( 'Error - insufficient permissions!', 'mainwp' );
1032 + $ret['error'] = $ret['result'];
1033 + MainWP_Client::exit_json( $ret );
1034 + }
1024 1035 if ( $client_id ) {
1025 1036 MainWP_DB_Client::instance()->delete_client( $client_id );
1026 1037 $ret['success'] = 'SUCCESS';
1027 1038 $ret['result'] = esc_html__( 'Client removed successfully.', 'mainwp' );
@@ -1054,8 +1065,12 @@
1054 1065 */
1055 1066 public function mainwp_clients_delete_general_field() {
1056 1067 $this->check_security( 'mainwp_clients_delete_general_field' );
1057 1068 $ret = array( 'success' => false );
1069 + if ( ! \mainwp_current_user_can( 'dashboard', 'manage_clients' ) ) {
1070 + $ret['error'] = esc_html__( 'Error - insufficient permissions!', 'mainwp' );
1071 + MainWP_Client::exit_json( $ret );
1072 + }
1058 1073 // phpcs:disable WordPress.Security.NonceVerification,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1059 1074 $field_id = isset( $_POST['field_id'] ) ? intval( $_POST['field_id'] ) : 0;
1060 1075 $client_id = 0; // 0 general fields.
1061 1076 if ( MainWP_DB_Client::instance()->delete_client_field_by( 'field_id', $field_id, $client_id ) ) {
@@ -1074,8 +1089,12 @@
1074 1089 */
1075 1090 public function mainwp_clients_bulk_delete_fields() {
1076 1091 $this->check_security( 'mainwp_clients_bulk_delete_fields' );
1077 1092 $ret = array( 'success' => false );
1093 + if ( ! \mainwp_current_user_can( 'dashboard', 'manage_clients' ) ) {
1094 + $ret['error'] = esc_html__( 'Error - insufficient permissions!', 'mainwp' );
1095 + MainWP_Client::exit_json( $ret );
1096 + }
1078 1097 // phpcs:disable WordPress.Security.NonceVerification,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1079 1098 $field_ids = isset( $_POST['field_ids'] ) && is_array( $_POST['field_ids'] ) ? array_map( 'intval', $_POST['field_ids'] ) : array();
1080 1099 // phpcs:enable WordPress.Security.NonceVerification,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1081 1100
@@ -1115,8 +1134,14 @@
1115 1134 // phpcs:disable WordPress.Security.NonceVerification,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1116 1135 $field_id = isset( $_POST['field_id'] ) ? intval( $_POST['field_id'] ) : 0;
1117 1136 $client_id = isset( $_POST['client_id'] ) ? intval( $_POST['client_id'] ) : 0; // $client_id > 0, individual token.
1118 1137 // phpcs:enable WordPress.Security.NonceVerification,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1138 + $field = MainWP_DB_Client::instance()->get_client_fields_by( 'field_id', $field_id );
1139 + if ( ! $field || ! $field->client_id || ! \mainwp_current_user_can( 'dashboard', 'manage_clients' ) || ! MainWP_Client::has_client_access( $field->client_id ) ) {
1140 + $ret['error'] = esc_html__( 'Error - insufficient permissions!', 'mainwp' );
1141 + MainWP_Client::exit_json( $ret );
1142 + }
1143 + $client_id = (int) $field->client_id;
1119 1144 if ( $client_id && MainWP_DB_Client::instance()->delete_client_field_by( 'field_id', $field_id, $client_id ) ) {
1120 1145 $ret['success'] = true;
1121 1146 }
1122 1147 echo wp_json_encode( $ret );
@@ -1138,15 +1163,50 @@
1138 1163
1139 1164 /**
1140 1165 * Method mainwp_clients_notes_save()
1141 1166 *
1142 - * Post handler for save notes on,
1143 - * Page: Manage Sites.
1167 + * Post handler for saving a client note.
1144 1168 *
1145 - * @uses \MainWP\Dashboard\MainWP_Manage_Sites_Handler::save_note()
1169 + * Requires the same page-level permission the Clients screen enforces,
1170 + * and an existing client within the caller's Team Control site scope.
1171 + *
1172 + * @uses \MainWP\Dashboard\MainWP_Client::save_note()
1146 1173 */
1147 1174 public function mainwp_clients_notes_save() {
1148 1175 $this->secure_request( 'mainwp_clients_notes_save' );
1176 +
1177 + if ( ! \mainwp_current_user_can( 'dashboard', 'manage_clients' ) ) {
1178 + MainWP_Client::exit_json(
1179 + array(
1180 + 'error' => sprintf(
1181 + /* translators: %s: page name. */
1182 + esc_html__( 'You do not have sufficient permissions to access this page (%s).', 'mainwp' ),
1183 + /* translators: Page name shown when client note access is denied. */
1184 + esc_html__( 'Manage Clients', 'mainwp' )
1185 + ),
1186 + )
1187 + );
1188 + }
1189 +
1190 + // phpcs:disable WordPress.Security.NonceVerification,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- nonce verified in secure_request().
1191 + $client_id = isset( $_POST['clientid'] ) ? intval( $_POST['clientid'] ) : 0;
1192 + // phpcs:enable WordPress.Security.NonceVerification,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1193 +
1194 + $client = $client_id ? MainWP_DB_Client::instance()->get_wp_client_by( 'client_id', $client_id ) : null;
1195 + if ( empty( $client ) ) {
1196 + /* translators: Error when the client id does not match a client. */
1197 + $missing_client = esc_html__( 'Client not found. Please try again.', 'mainwp' );
1198 + MainWP_Client::exit_json(
1199 + array(
1200 + 'error' => $missing_client,
1201 + )
1202 + );
1203 + }
1204 +
1205 + if ( ! MainWP_Client::has_client_access( $client_id ) ) {
1206 + MainWP_Client::exit_json( array( 'error' => esc_html__( 'Error - insufficient permissions!', 'mainwp' ) ) );
1207 + }
1208 +
1149 1209 MainWP_Client::save_note();
1150 1210 }
1151 1211
1152 1212 /**
@@ -1158,8 +1218,13 @@
1158 1218 * @uses \MainWP\Dashboard\MainWP_Manage_Sites_Handler::save_note()
1159 1219 */
1160 1220 public function mainwp_clients_suspend_client() {
1161 1221 $this->secure_request( 'mainwp_clients_suspend_client' );
1222 +
1223 + if ( ! \mainwp_current_user_can( 'dashboard', 'manage_clients' ) ) {
1224 + wp_die( 'Error - insufficient permissions!' );
1225 + }
1226 +
1162 1227 // phpcs:disable WordPress.Security.NonceVerification,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1163 1228 $clientid = isset( $_POST['clientid'] ) ? intval( $_POST['clientid'] ) : 0;
1164 1229 $suspended = isset( $_POST['suspend_status'] ) && ! empty( $_POST['suspend_status'] ) ? 1 : 0;
1165 1230 // phpcs:enable WordPress.Security.NonceVerification,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
@@ -1167,8 +1232,13 @@
1167 1232 if ( empty( $clientid ) ) {
1168 1233 wp_die( 'Error - empty client id!' );
1169 1234 }
1170 1235
1236 + $client = MainWP_DB_Client::instance()->get_wp_client_by( 'client_id', $clientid );
1237 + if ( ! $client || ! MainWP_Client::has_client_access( $clientid, true ) ) {
1238 + wp_die( esc_html__( 'Error - insufficient permissions!', 'mainwp' ) );
1239 + }
1240 +
1171 1241 $params = array(
1172 1242 'client_id' => $clientid,
1173 1243 'suspended' => $suspended,
1174 1244 );
@@ -1222,8 +1292,11 @@
1222 1292 * @uses \MainWP_DB_Client::instance()->update_selected_sites_for_client()
1223 1293 */
1224 1294 public function mainwp_clients_import_client() { // phpcs:ignore -- NOSONAR
1225 1295 $this->secure_request( 'mainwp_clients_import_client' );
1296 + if ( ! \mainwp_current_user_can( 'dashboard', 'manage_clients' ) ) {
1297 + return wp_send_json_error( array( 'message' => esc_html__( 'Error - insufficient permissions!', 'mainwp' ) ) );
1298 + }
1226 1299 // phpcs:disable
1227 1300 // Set value from POST data.
1228 1301 $default_values = array(
1229 1302 'client.name' => ! empty( $_POST['name'] ) ? sanitize_text_field( wp_unslash( $_POST['name'] ) ) : '',
@@ -1260,16 +1333,27 @@
1260 1333 }
1261 1334 }
1262 1335
1263 1336 if ( ! empty( $client_to_add ) ) {
1337 + $allowed = MainWP_Abilities_Util::get_allowed_site_ids();
1338 + $current = MainWP_DB_Client::instance()->get_wp_client_by( 'client_email', $default_values['client.email'] );
1339 + if ( ( 'all' !== $allowed && array_diff( array_map( 'intval', $selected_sites ), $allowed ) ) || ( $current && ( ! MainWP_Client::has_client_access( $current->client_id ) || ( isset( $client_to_add['suspended'] ) && (int) $client_to_add['suspended'] !== (int) $current->suspended && ! MainWP_Client::has_client_access( $current->client_id, true ) ) ) ) ) {
1340 + return wp_send_json_error( array( 'message' => esc_html__( 'Error - insufficient permissions!', 'mainwp' ) ) );
1341 + }
1264 1342 // Set default values.
1265 1343 $client_to_add['created'] = time();
1266 1344 $client_to_add['primary_contact_id'] = 0;
1345 + if ( $current && 'all' !== $allowed ) {
1346 + $client_to_add['client_id'] = $current->client_id;
1347 + unset( $client_to_add['created'], $client_to_add['primary_contact_id'] );
1348 + $sites = MainWP_DB_Client::instance()->get_websites_by_client_ids( $current->client_id, array( 'no_perm_check' => true ) );
1349 + $selected_sites = MainWP_System::get_selected_sites_with_allowed_sites( $selected_sites, wp_list_pluck( (array) $sites, 'id' ) );
1350 + }
1267 1351 // Inset client.
1268 1352 $inserted = MainWP_DB_Client::instance()->update_client( $client_to_add, true );
1269 1353
1270 1354 if ( ! empty( $inserted ) && is_object( $inserted ) ) {
1271 - if ( ! empty( $selected_sites ) ) {
1355 + if ( ! empty( $selected_sites ) || ( $current && 'all' !== $allowed ) ) {
1272 1356 MainWP_DB_Client::instance()->update_selected_sites_for_client( $inserted->client_id, $selected_sites ); // Set client to selected sites.
1273 1357 }
1274 1358 // Set default color and icon .
1275 1359 $update = array(
@@ -1352,28 +1436,8 @@
1352 1436 die( 'ok' );
1353 1437 }
1354 1438
1355 1439 /**
1356 - * Method mainwp_showhide_sections()
1357 - *
1358 - * Show/Hide sections.
1359 - */
1360 - public function mainwp_showhide_sections() {
1361 - // phpcs:disable WordPress.Security.NonceVerification,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1362 - if ( isset( $_POST['sec'] ) && isset( $_POST['status'] ) ) {
1363 - $opts = get_option( 'mainwp_opts_showhide_sections' );
1364 - if ( ! is_array( $opts ) ) {
1365 - $opts = array();
1366 - }
1367 - $opts[ sanitize_text_field( wp_unslash( $_POST['sec'] ) ) ] = sanitize_text_field( wp_unslash( $_POST['status'] ) );
1368 - update_option( 'mainwp_opts_showhide_sections', $opts );
1369 - die( 'ok' );
1370 - }
1371 - // phpcs:enable WordPress.Security.NonceVerification,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1372 - die( 'failed' );
1373 - }
1374 -
1375 - /**
1376 1440 * Method mainwp_saving_status()
1377 1441 *
1378 1442 * MainWP Saving Status.
1379 1443 */
@@ -1470,8 +1534,11 @@
1470 1534 *
1471 1535 * @uses \MainWP\Dashboard\MainWP_Plugins_Handler::trust_plugin()
1472 1536 */
1473 1537 public function mainwp_autoupdate_and_trust_child() {
1538 + if ( ! \mainwp_current_user_can( 'dashboard', 'manage_dashboard_settings' ) ) {
1539 + MainWP_Client::exit_json( array( 'error' => esc_html__( 'Error - insufficient permissions!', 'mainwp' ) ) );
1540 + }
1474 1541 $this->secure_request( 'mainwp_autoupdate_and_trust_child' );
1475 1542 if ( 1 !== (int) get_option( 'mainwp_pluginAutomaticDailyUpdate' ) ) {
1476 1543 update_option( 'mainwp_pluginAutomaticDailyUpdate', 1 );
1477 1544 }
@@ -1653,8 +1720,14 @@
1653 1720 /**
1654 1721 * Method ajax_import_demo_data().
1655 1722 */
1656 1723 public function ajax_import_demo_data() {
1724 + if ( ! MainWP_System_Utility::is_admin() ) {
1725 + die( 0 );
1726 + }
1727 + if ( ! \mainwp_current_user_can( 'dashboard', 'manage_dashboard_settings' ) ) {
1728 + wp_die( wp_json_encode( array( 'error' => esc_html__( 'Error - insufficient permissions!', 'mainwp' ) ) ) );
1729 + }
1657 1730 $this->secure_request( 'mainwp_import_demo_data' );
1658 1731 $data = MainWP_Demo_Handle::get_instance()->import_data_demo();
1659 1732 MainWP_Utility::update_option( 'mainwp_enable_guided_tours', 1 );
1660 1733 wp_die( wp_json_encode( $data ) );
@@ -1663,8 +1736,14 @@
1663 1736 /**
1664 1737 * Method ajax_delete_demo_data().
1665 1738 */
1666 1739 public function ajax_delete_demo_data() {
1740 + if ( ! MainWP_System_Utility::is_admin() ) {
1741 + die( 0 );
1742 + }
1743 + if ( ! \mainwp_current_user_can( 'dashboard', 'manage_dashboard_settings' ) ) {
1744 + wp_die( wp_json_encode( array( 'error' => esc_html__( 'Error - insufficient permissions!', 'mainwp' ) ) ) );
1745 + }
1667 1746 $this->secure_request( 'mainwp_delete_demo_data' );
1668 1747 $data = MainWP_Demo_Handle::get_instance()->delete_data_demo();
1669 1748 MainWP_Utility::update_option( 'mainwp_enable_guided_tours', 0 );
1670 1749 wp_die( wp_json_encode( $data ) );
@@ -1809,24 +1888,30 @@
1809 1888 /**
1810 1889 * Method ajax_import_website_add_client()
1811 1890 *
1812 1891 * Create client.
1892 + *
1893 + * @throws MainWP_Exception When the submitted client data is malformed.
1813 1894 */
1814 1895 public function ajax_import_website_add_client() {
1815 1896 $this->secure_request( 'mainwp_import_website_add_client' ); // Check secure.
1897 + if ( ! \mainwp_current_user_can( 'dashboard', 'manage_clients' ) ) {
1898 + return wp_send_json_error( esc_html__( 'Error - insufficient permissions!', 'mainwp' ) );
1899 + }
1816 1900 $error_msg = esc_html__( 'Undefined error. Please try again.', 'mainwp' );
1817 1901 try {
1818 1902 // Retrieve client data.
1819 - $data = $this->mainwp_get_sanitized_post( 'client' );
1903 + $data = isset( $_POST['client'] ) && is_string( $_POST['client'] ) ? wp_unslash( $_POST['client'] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Encoded fields must be normalized after JSON decoding.
1820 1904 $site_id = $this->mainwp_get_sanitized_post( 'site_id' );
1821 1905
1822 1906 if ( empty( $data ) ) {
1823 - wp_die( wp_send_json_error( esc_html( $error_msg ) ) ); //phpcs:ignore WordPress.Security.EscapeOutput
1907 + throw new MainWP_Exception( __( 'The client data could not be read. Please try again.', 'mainwp' ) );
1824 1908 }
1825 1909
1826 - $client_data = json_decode( $data, true );
1910 + $decoded = json_decode( $data );
1911 + $client_data = $this->normalize_client_import_row( $decoded instanceof \stdClass ? get_object_vars( $decoded ) : null );
1827 1912 // Create client.
1828 - $client = $this->mainwp_handle_create_client( $client_data );
1913 + $client = $this->mainwp_handle_create_client( $client_data, empty( $site_id ) ? array() : array( $site_id ) );
1829 1914 if ( $client ) {
1830 1915 // Add groups website and client.
1831 1916 if ( ! empty( $site_id ) ) {
1832 1917 $this->mainwp_handle_selected_sites_for_client( $client, $site_id );
@@ -1834,9 +1919,9 @@
1834 1919
1835 1920 wp_die( wp_send_json_success( esc_html__( 'Created client successfully.', 'mainwp' ) ) ); //phpcs:ignore WordPress.Security.EscapeOutput
1836 1921 }
1837 1922 } catch ( \Exception $e ) {
1838 - wp_die( wp_send_json_error( sanitize_text_field( $e->getMessage() ) ) ); //phpcs:ignore WordPress.Security.EscapeOutput
1923 + wp_die( wp_send_json_error( esc_html( $e->getMessage() ) ) ); //phpcs:ignore WordPress.Security.EscapeOutput
1839 1924 }
1840 1925 // In case of no created.
1841 1926 wp_die( wp_send_json_error( esc_html( $error_msg ) ) ); //phpcs:ignore WordPress.Security.EscapeOutput
1842 1927 }
@@ -1844,25 +1929,42 @@
1844 1929 /**
1845 1930 * Method ajax_import_website_add_client_no_site()
1846 1931 *
1847 1932 * Create client dont has a website.
1933 + *
1934 + * @throws MainWP_Exception When the submitted client data is malformed.
1848 1935 */
1849 1936 public function ajax_import_website_add_client_no_site() {
1850 1937 $this->secure_request( 'mainwp_import_website_add_client_no_site' ); // Check secure.
1938 + if ( ! \mainwp_current_user_can( 'dashboard', 'manage_clients' ) ) {
1939 + return wp_send_json_error( esc_html__( 'Error - insufficient permissions!', 'mainwp' ) );
1940 + }
1851 1941 $error_msg = esc_html__( 'Undefined error. Please try again.', 'mainwp' );
1852 1942 try {
1853 - $data = ! empty( $_POST['client'] ) ? rest_sanitize_array( $_POST['client'] ) : array(); //phpcs:ignore -- NonceVerification.
1943 + $data = ! empty( $_POST['client'] ) ? wp_unslash( $_POST['client'] ) : array(); // phpcs:ignore WordPress.Security.NonceVerification,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Encoded fields must be normalized after JSON decoding.
1854 1944
1855 1945 if ( empty( $data ) || ! is_array( $data ) ) {
1856 - wp_die( wp_send_json_error( esc_html( $error_msg ) ) ); //phpcs:ignore WordPress.Security.EscapeOutput
1946 + throw new MainWP_Exception( __( 'The client data could not be read. Please try again.', 'mainwp' ) );
1857 1947 }
1858 1948
1949 + $rows = array();
1859 1950 foreach ( $data as $val_data ) {
1860 - $client_data = json_decode( stripslashes( $val_data ), true ); // Decode client data.
1861 - $this->mainwp_handle_create_client( $client_data ); // Update or instert new client.
1951 + $decoded = is_string( $val_data ) ? json_decode( $val_data ) : null;
1952 + $client_data = $this->normalize_client_import_row( $decoded instanceof \stdClass ? get_object_vars( $decoded ) : null );
1953 + $rows[] = array_merge( $client_data, array( 'client_name' => $client_data['name'] ) );
1862 1954 }
1955 + $plan = $this->plan_multi_client_import( $rows );
1956 + $created = array();
1957 + foreach ( $plan as $write ) {
1958 + $client = $write['client'] ? $write['client'] : ( $created[ $write['key'] ] ?? null );
1959 + if ( ! $client ) {
1960 + $client_data = $write['row'];
1961 + unset( $client_data['client_name'] );
1962 + $created[ $write['key'] ] = $this->mainwp_handle_create_client( $client_data );
1963 + }
1964 + }
1863 1965 } catch ( \Exception $e ) {
1864 - wp_die( wp_send_json_error( sanitize_text_field( $e->getMessage() ) ) ); //phpcs:ignore WordPress.Security.EscapeOutput
1966 + wp_die( wp_send_json_error( esc_html( $e->getMessage() ) ) ); //phpcs:ignore WordPress.Security.EscapeOutput
1865 1967 }
1866 1968 // In case of no created.
1867 1969 wp_die( wp_send_json_error( esc_html( $error_msg ) ) ); //phpcs:ignore WordPress.Security.EscapeOutput
1868 1970 }
@@ -1870,16 +1972,31 @@
1870 1972 /**
1871 1973 * Method ajax_clients_add_multi_client
1872 1974 *
1873 1975 * Create Multi Client form QSW.
1976 + *
1977 + * @throws MainWP_Exception When a row is outside client or site scope.
1874 1978 */
1875 1979 public function ajax_clients_add_multi_client() {
1876 1980 $error_msg = esc_html__( 'Undefined error. Please try again.', 'mainwp' );
1877 1981 $this->check_security( 'mainwp_clients_add_multi_client' );
1878 - $dataes = isset( $_POST['data'] ) ? $_POST['data'] : array(); //phpcs:ignore -- NonceVerification.
1982 + if ( ! \mainwp_current_user_can( 'dashboard', 'manage_clients' ) ) {
1983 + return wp_send_json_error( esc_html__( 'Error - insufficient permissions!', 'mainwp' ) );
1984 + }
1985 + $dataes = array_key_exists( 'data', $_POST ) ? $_POST['data'] : array(); //phpcs:ignore -- NonceVerification.
1879 1986 try {
1987 + if ( ! is_array( $dataes ) ) {
1988 + throw new MainWP_Exception( __( 'The client data could not be read. Please try again.', 'mainwp' ) );
1989 + }
1880 1990 if ( ! empty( $dataes ) ) {
1881 - foreach ( $dataes as $val_data ) {
1991 + foreach ( $dataes as $key => $row ) {
1992 + $dataes[ $key ] = $this->normalize_client_import_row( $row, true );
1993 + }
1994 + $plan = $this->plan_multi_client_import( $dataes );
1995 + $created = array();
1996 + // Access belongs to the whole plan; checking mutated DB state here could leave partial writes.
1997 + foreach ( $plan as $write ) {
1998 + $val_data = $write['row'];
1882 1999 $client_data = array(
1883 2000 'image' => '',
1884 2001 'name' => $val_data['client_name'] ?? '',
1885 2002 'address_1' => '',
@@ -1899,18 +2016,22 @@
1899 2016 'suspended' => 0,
1900 2017 'primary_contact_id' => 0,
1901 2018 );
1902 2019
1903 - // Create client.
1904 - $client = $this->mainwp_handle_create_client( $client_data );
1905 -
1906 - // Create client successfy.
1907 - if ( ! empty( $client ) && ! empty( $val_data['website_id'] ) ) {
1908 - // Add groups website and client.
1909 - $this->mainwp_handle_selected_sites_for_client( $client, $val_data['website_id'] );
2020 + $client = ! empty( $write['client'] ) ? $write['client'] : ( $created[ $write['key'] ] ?? null );
2021 + if ( ! $client ) {
2022 + $client = MainWP_DB_Client::instance()->update_client( $client_data );
2023 + if ( empty( $client ) ) {
2024 + // The plan already moved this row's sites to the new client; later rows would write rosters that detach them.
2025 + throw new MainWP_Exception( __( 'A client could not be saved, so the import stopped. Clients before it in the list may have been added; check the Clients page.', 'mainwp' ) );
2026 + }
2027 + $created[ $write['key'] ] = $client;
1910 2028 }
2029 + if ( ! empty( $client ) && null !== $write['site_ids'] ) {
2030 + MainWP_DB_Client::instance()->update_selected_sites_for_client( $client->client_id, $write['site_ids'] );
2031 + }
1911 2032 // Create client contact.
1912 - if ( ! empty( $val_data['contacts_field'] ) ) {
2033 + if ( ! empty( $client ) && ! empty( $val_data['contacts_field'] ) ) {
1913 2034 $this->mainwp_handle_create_contact_for_client( $client, $val_data['contacts_field'] );
1914 2035 }
1915 2036 }
1916 2037 wp_die( wp_send_json_success( esc_html__( 'Created client successfully.', 'mainwp' ) ) ); //phpcs:ignore WordPress.Security.EscapeOutput
@@ -1915,9 +2036,9 @@
1915 2036 }
1916 2037 wp_die( wp_send_json_success( esc_html__( 'Created client successfully.', 'mainwp' ) ) ); //phpcs:ignore WordPress.Security.EscapeOutput
1917 2038 }
1918 2039 } catch ( \Exception $e ) {
1919 - wp_die( wp_send_json_error( sanitize_text_field( $e->getMessage() ) ) ); //phpcs:ignore WordPress.Security.EscapeOutput
2040 + wp_die( wp_send_json_error( esc_html( $e->getMessage() ) ) ); //phpcs:ignore WordPress.Security.EscapeOutput
1920 2041 }
1921 2042
1922 2043 wp_die( wp_send_json_error( esc_html( $error_msg ) ) ); //phpcs:ignore WordPress.Security.EscapeOutput
1923 2044 }
@@ -1925,8 +2046,14 @@
1925 2046 /**
1926 2047 * Method ajax_increase_connection_security
1927 2048 */
1928 2049 public function ajax_increase_connection_security() {
2050 + if ( ! MainWP_System_Utility::is_admin() ) {
2051 + die( 0 );
2052 + }
2053 + if ( ! \mainwp_current_user_can( 'dashboard', 'manage_dashboard_settings' ) ) {
2054 + wp_die( wp_json_encode( array( 'error' => esc_html__( 'Error - insufficient permissions!', 'mainwp' ) ) ) );
2055 + }
1929 2056 $this->check_security( 'mainwp_increase_connection_security' );
1930 2057 $websites = MainWP_DB::instance()->query( MainWP_DB::instance()->get_sql_websites_for_current_user( false, null, 'wp.url', false, false, null, true ) );
1931 2058 while ( $websites && ( $website = MainWP_DB::fetch_object( $websites ) ) ) {
1932 2059 // try to decrypt priv key.
@@ -1979,9 +2106,9 @@
1979 2106
1980 2107 $siteId = isset( $_POST['siteId'] ) ? intval( $_POST['siteId'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1981 2108 $type = isset( $_POST['type'] ) ? sanitize_text_field( wp_unslash( $_POST['type'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1982 2109 $slug = isset( $_POST['slug'] ) ? sanitize_text_field( wp_unslash( $_POST['slug'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1983 - $name = isset( $_POST['name'] ) ? sanitize_text_field( wp_unslash( urldecode( $_POST['name'] ) ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
2110 + $name = isset( $_POST['name'] ) ? sanitize_text_field( urldecode( wp_unslash( $_POST['name'] ) ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1984 2111
1985 2112 if ( 'plugin' === $type && ! empty( $slug ) ) {
1986 2113 if ( false !== strpos( $slug, '/' ) ) {
1987 2114 $parts = explode( '/', $slug );
@@ -2054,8 +2181,278 @@
2054 2181 return isset( $_POST[ $key ] ) ? $callback( wp_unslash( $_POST[ $key ] ) ) : ''; //phpcs:ignore -- NonceVerification.
2055 2182 }
2056 2183
2057 2184 /**
2185 + * Import encoding must not bypass email lookup or allow stored markup.
2186 + *
2187 + * @param array $row Decoded client or Quick Setup row.
2188 + * @param bool $multi_client Whether the row uses Quick Setup field names.
2189 + * @return array Normalized row.
2190 + * @throws MainWP_Exception When a row is malformed or an email is invalid.
2191 + */
2192 + private function normalize_client_import_row( $row, $multi_client = false ) {
2193 + $client_data = array(
2194 + 'image' => '',
2195 + 'name' => '',
2196 + 'address_1' => '',
2197 + 'address_2' => '',
2198 + 'city' => '',
2199 + 'zip' => '',
2200 + 'state' => '',
2201 + 'country' => '',
2202 + 'note' => '',
2203 + 'selected_icon_info' => 'selected:wordpress;color:#34424d',
2204 + 'client_email' => '',
2205 + 'client_phone' => '',
2206 + 'client_facebook' => '',
2207 + 'client_twitter' => '',
2208 + 'client_instagram' => '',
2209 + 'client_linkedin' => '',
2210 + 'suspended' => 0,
2211 + 'primary_contact_id' => 0,
2212 + );
2213 + $name_field = $multi_client ? 'client_name' : 'name';
2214 + $fields = $multi_client ? array( 'client_name', 'client_email' ) : array_keys( $client_data );
2215 + if ( ! is_array( $row ) ) {
2216 + throw new MainWP_Exception( __( 'The client data could not be read. Please try again.', 'mainwp' ) ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Import handlers escape the message when sending the response.
2217 + }
2218 + foreach ( $fields as $field ) {
2219 + if ( array_key_exists( $field, $row ) && ! is_string( $row[ $field ] ) ) {
2220 + throw new MainWP_Exception( __( 'The client data could not be read. Please try again.', 'mainwp' ) ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Import handlers escape the message when sending the response.
2221 + }
2222 + }
2223 + if ( array_key_exists( 'contacts_field', $row ) ) {
2224 + if ( ! $multi_client && $row['contacts_field'] instanceof \stdClass ) {
2225 + $row['contacts_field'] = get_object_vars( $row['contacts_field'] );
2226 + }
2227 + if ( ! is_array( $row['contacts_field'] ) ) {
2228 + throw new MainWP_Exception( __( 'The client data could not be read. Please try again.', 'mainwp' ) ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Import handlers escape the message when sending the response.
2229 + }
2230 + foreach ( $row['contacts_field'] as $value ) {
2231 + if ( ! is_string( $value ) ) {
2232 + throw new MainWP_Exception( __( 'The client data could not be read. Please try again.', 'mainwp' ) ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Import handlers escape the message when sending the response.
2233 + }
2234 + }
2235 + }
2236 + // JSON fields were already unslashed before decoding; Quick Setup fields are still request data.
2237 + if ( $multi_client ) {
2238 + $row = wp_unslash( $row );
2239 + }
2240 + $name = $row[ $name_field ] ?? '';
2241 + $row[ $name_field ] = sanitize_text_field( $name );
2242 + $row['client_email'] = $this->normalize_client_import_email( $row['client_email'] ?? '' );
2243 + if ( ( '' !== $name && '' === $row[ $name_field ] ) || ( '' === $row[ $name_field ] && '' === $row['client_email'] ) ) {
2244 + throw new MainWP_Exception( __( 'The client data could not be read. Please try again.', 'mainwp' ) ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Import handlers escape the message when sending the response.
2245 + }
2246 + if ( ! empty( $row['contacts_field'] ) ) {
2247 + foreach ( array( 'contact_name', 'contact_role' ) as $field ) {
2248 + $row['contacts_field'][ $field ] = sanitize_text_field( $row['contacts_field'][ $field ] ?? '' );
2249 + }
2250 + $row['contacts_field']['contact_email'] = $this->normalize_client_import_email( $row['contacts_field']['contact_email'] ?? '' );
2251 + }
2252 + if ( $multi_client ) {
2253 + return $row;
2254 + }
2255 + $client_data['name'] = $row['name'];
2256 + $client_data['client_email'] = $row['client_email'];
2257 + foreach ( array( 'address_1', 'address_2', 'city', 'zip', 'state', 'country', 'client_phone' ) as $field ) {
2258 + $client_data[ $field ] = sanitize_text_field( $row[ $field ] ?? '' );
2259 + }
2260 + $client_data['note'] = MainWP_Utility::sanitize_client_note( $row['note'] ?? '' );
2261 + foreach ( array( 'client_facebook', 'client_twitter', 'client_instagram', 'client_linkedin' ) as $field ) {
2262 + $client_data[ $field ] = esc_url_raw( $row[ $field ] ?? '' );
2263 + }
2264 + // Imported identifiers and image paths must never select an update or a file to delete.
2265 + return $client_data;
2266 + }
2267 +
2268 + /**
2269 + * Rewriting an email could select or create a different client or contact.
2270 + *
2271 + * @param string $email Unslashed submitted email.
2272 + * @return string Normalized email.
2273 + * @throws MainWP_Exception When an email is invalid or sanitizing would rewrite it.
2274 + */
2275 + private function normalize_client_import_email( $email ) {
2276 + $trimmed = trim( $email );
2277 + $normalized = sanitize_email( $trimmed );
2278 + if ( '' !== $email && ( $normalized !== $trimmed || ! is_email( $normalized ) ) ) {
2279 + throw new MainWP_Exception( __( 'One or more email addresses are not valid. No clients were added.', 'mainwp' ) ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Import handlers escape the message when sending the response.
2280 + }
2281 + return $normalized;
2282 + }
2283 +
2284 + /**
2285 + * Column refusals must precede inserts and projected site moves.
2286 + *
2287 + * @param string $name Normalized client name.
2288 + * @param string $email Normalized client email.
2289 + * @throws MainWP_Exception When either column cannot store its value.
2290 + */
2291 + private function validate_client_import_columns( $name, $email ) {
2292 + global $wpdb;
2293 + $table = MainWP_DB_Client::instance()->get_table_name( 'wp_clients' );
2294 + $limits = array(
2295 + 'name' => $wpdb->get_col_length( $table, 'name' ),
2296 + 'client_email' => $wpdb->get_col_length( $table, 'client_email' ),
2297 + );
2298 + $values = array(
2299 + 'name' => $name,
2300 + 'client_email' => $email,
2301 + );
2302 + foreach ( $values as $column => $value ) {
2303 + if ( is_array( $limits[ $column ] ) && mb_strlen( $value, 'UTF-8' ) > $limits[ $column ]['length'] ) {
2304 + /* translators: 1: Maximum client name length, 2: Maximum client email address length. */
2305 + throw new MainWP_Exception( sprintf( __( 'A client name or email address is too long. Names can have up to %1$d characters and email addresses up to %2$d. No clients were added.', 'mainwp' ), $limits['name']['length'], $limits['client_email']['length'] ) ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Import handlers escape the message when sending the response.
2306 + }
2307 + if ( $wpdb->strip_invalid_text_for_column( $table, $column, $value ) !== $value ) {
2308 + throw new MainWP_Exception( __( 'The client data could not be read. Please try again.', 'mainwp' ) ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Import handlers escape the message when sending the response.
2309 + }
2310 + }
2311 + }
2312 +
2313 + /**
2314 + * Plan ordered imports against projected ownership so a refusal precedes every write.
2315 + *
2316 + * @param array $rows Submitted rows.
2317 + * @return array Authorized writes in request order.
2318 + * @throws MainWP_Exception When a row is outside client or site scope.
2319 + */
2320 + private function plan_multi_client_import( $rows ) {
2321 + $allowed = MainWP_Abilities_Util::get_allowed_site_ids();
2322 + $websites = array();
2323 + $owners = array();
2324 + $emails = array();
2325 + $clients = array();
2326 + $references = array();
2327 + $rosters = array();
2328 + $new_emails = array();
2329 + $collation = null;
2330 + $plan = array();
2331 +
2332 + if ( ! is_array( $rows ) ) {
2333 + throw new MainWP_Exception( __( 'The client data could not be read. Please try again.', 'mainwp' ) ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Import handlers escape the message when sending the response.
2334 + }
2335 + foreach ( $rows as $row ) {
2336 + // The contact write runs after the row's client and site writes, so a shape it cannot take must be refused here.
2337 + if ( ! is_array( $row ) || ( isset( $row['client_name'] ) && ! is_string( $row['client_name'] ) ) || ( isset( $row['client_email'] ) && ! is_string( $row['client_email'] ) ) || ( ! empty( $row['contacts_field'] ) && ( ! is_array( $row['contacts_field'] ) || array_filter( $row['contacts_field'], 'is_array' ) ) ) ) {
2338 + throw new MainWP_Exception( __( 'The client data could not be read. Please try again.', 'mainwp' ) ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Import handlers escape the message when sending the response.
2339 + }
2340 + if ( ! empty( $row['website_id'] ) && ( ! MainWP_Utility::ctype_digit( $row['website_id'] ) || (int) $row['website_id'] <= 0 ) ) {
2341 + throw new MainWP_Exception( __( 'Error - insufficient permissions!', 'mainwp' ) ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Import handlers escape the message when sending the response.
2342 + }
2343 + $site_id = empty( $row['website_id'] ) ? 0 : (int) $row['website_id'];
2344 + if ( $site_id && ! array_key_exists( $site_id, $websites ) ) {
2345 + $website = MainWP_DB::instance()->get_website_by_id_unscoped( $row['website_id'] );
2346 + if ( ! MainWP_System_Utility::can_edit_website( $website ) || ( 'all' !== $allowed && ! in_array( (int) $website->id, $allowed, true ) ) ) {
2347 + throw new MainWP_Exception( __( 'Error - insufficient permissions!', 'mainwp' ) ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Import handlers escape the message when sending the response.
2348 + }
2349 + $websites[ $site_id ] = $website;
2350 + $owners[ $site_id ] = 'client:' . $website->client_id;
2351 + }
2352 + $email = $row['client_email'] ?? '';
2353 + if ( ! array_key_exists( $email, $emails ) ) {
2354 + $emails[ $email ] = MainWP_DB_Client::instance()->get_wp_client_by( 'client_email', $email, OBJECT );
2355 + }
2356 + $client = $emails[ $email ];
2357 + $key = $client ? 'client:' . $client->client_id : null;
2358 + if ( $client ) {
2359 + $clients[ $key ] = $client;
2360 + $rosters[ $key ] = array();
2361 + }
2362 + $references[] = array(
2363 + 'row' => $row,
2364 + 'client' => $client,
2365 + 'key' => $key,
2366 + 'site_id' => $site_id,
2367 + );
2368 + }
2369 + if ( $clients ) {
2370 + $sites = MainWP_DB_Client::instance()->get_websites_by_client_ids( array_column( $clients, 'client_id' ), array( 'no_perm_check' => true ) );
2371 + foreach ( (array) $sites as $site ) {
2372 + $key = 'client:' . $site->client_id;
2373 + $rosters[ $key ][ (int) $site->id ] = (int) $site->id;
2374 + $owners[ (int) $site->id ] = $key;
2375 + }
2376 + }
2377 +
2378 + foreach ( $references as $reference ) {
2379 + $row = $reference['row'];
2380 + $key = $reference['key'];
2381 + $site_id = $reference['site_id'];
2382 + $email = $row['client_email'] ?? '';
2383 + if ( null === $key && ! empty( $email ) ) {
2384 + foreach ( $new_emails as $new_key => $new_email ) {
2385 + if ( $this->client_emails_match( $new_email, $email, 'all' !== $allowed, $collation ) ) {
2386 + $key = $new_key;
2387 + break;
2388 + }
2389 + }
2390 + }
2391 + if ( null !== $key ) {
2392 + if ( 'all' !== $allowed && ! array_intersect( $rosters[ $key ], $allowed ) ) {
2393 + throw new MainWP_Exception( __( 'Error - insufficient permissions!', 'mainwp' ) ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Import handlers escape the message when sending the response.
2394 + }
2395 + } else {
2396 + $this->validate_client_import_columns( $row['client_name'] ?? '', $email );
2397 + $key = 'new:' . count( $plan );
2398 + $rosters[ $key ] = array();
2399 + // Emails PHP reads as empty are stored too, and update_client() treats a later padded copy as a duplicate.
2400 + $new_emails[ $key ] = $email;
2401 + }
2402 + if ( $site_id ) {
2403 + // Reassignment can remove the overlap needed to edit a later row's client.
2404 + $owner = $owners[ $site_id ];
2405 + unset( $rosters[ $owner ][ $site_id ] );
2406 + $rosters[ $key ][ $site_id ] = $site_id;
2407 + $owners[ $site_id ] = $key;
2408 + }
2409 + $plan[] = array(
2410 + 'row' => $row,
2411 + 'key' => $key,
2412 + 'client' => $reference['client'],
2413 + // Complete projected rosters keep hidden sites without reclaiming sites moved earlier.
2414 + 'site_ids' => $site_id ? array_values( $rosters[ $key ] ) : null,
2415 + );
2416 + }
2417 + return $plan;
2418 + }
2419 +
2420 + /**
2421 + * Match pending clients using the email column's equality rules.
2422 + *
2423 + * @param string $first First email.
2424 + * @param string $second Second email.
2425 + * @param bool $restricted Whether equality must fail closed.
2426 + * @param array|null $collation Cached charset and collation for this plan.
2427 + * @return bool Whether the emails match.
2428 + * @throws MainWP_Exception When restricted equality cannot be evaluated.
2429 + */
2430 + private function client_emails_match( $first, $second, $restricted, &$collation ) {
2431 + global $wpdb;
2432 + $table = MainWP_DB_Client::instance()->get_table_name( 'wp_clients' );
2433 + if ( ! $restricted ) {
2434 + // The empty column branch preserves lookup equality when collation metadata is unavailable.
2435 + $sql = $wpdb->prepare( 'SELECT client_email = %s FROM (SELECT client_email FROM %i WHERE 1=0 UNION ALL SELECT %s) AS pending_email', $second, $table, $first );
2436 + } else {
2437 + if ( null === $collation ) {
2438 + $column = $wpdb->get_row( $wpdb->prepare( 'SHOW FULL COLUMNS FROM %i LIKE %s', $table, 'client_email' ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- column metadata, once per plan requiring pending-email comparison.
2439 + if ( ! $column || ! isset( $column->Collation ) || ! preg_match( '/^([a-z0-9]+)_[a-z0-9_]+$/i', (string) $column->Collation, $matches ) ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- MySQL column name.
2440 + throw new MainWP_Exception( __( 'Error - insufficient permissions!', 'mainwp' ) ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Import handlers escape the message when sending the response.
2441 + }
2442 + $collation = array( $matches[1], $column->Collation ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- MySQL column name.
2443 + }
2444 + // The column's allowlisted charset and collation preserve the lookup's equality rules.
2445 + $sql = $wpdb->prepare( 'SELECT CONVERT(%s USING ' . $collation[0] . ') COLLATE ' . $collation[1] . ' = CONVERT(%s USING ' . $collation[0] . ') COLLATE ' . $collation[1], $first, $second ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- charset and collation allowlisted above.
2446 + }
2447 + $equal = $wpdb->get_var( $sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery,WordPress.DB.PreparedSQL.NotPrepared -- prepared value comparison under the email column's collation.
2448 + if ( $restricted && null === $equal ) {
2449 + throw new MainWP_Exception( __( 'Error - insufficient permissions!', 'mainwp' ) ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Import handlers escape the message when sending the response.
2450 + }
2451 + return '1' === (string) $equal;
2452 + }
2453 +
2454 + /**
2058 2455 * Method mainwp_handle_create_client()
2059 2456 *
2060 2457 * Create new client or return client data.
2061 2458 *
@@ -2062,16 +2459,29 @@
2062 2459 * @uses MainWP_DB_Client::instance()->get_wp_client_by()
2063 2460 * @uses MainWP_DB_Client::instance()->update_client()
2064 2461 *
2065 2462 * @param array $client_data new client creation data.
2463 + * @param array $site_ids Sites to attach in this import row.
2066 2464 *
2067 2465 * @return object client data.
2466 + * @throws MainWP_Exception If the client or submitted sites are outside UI scope.
2068 2467 */
2069 - public function mainwp_handle_create_client( $client_data ) {
2468 + public function mainwp_handle_create_client( $client_data, $site_ids = array() ) {
2469 + $allowed = MainWP_Abilities_Util::get_allowed_site_ids();
2470 + if ( ! \mainwp_current_user_can( 'dashboard', 'manage_clients' ) || ( 'all' !== $allowed && array_diff( array_map( 'intval', $site_ids ), $allowed ) ) ) {
2471 + throw new MainWP_Exception( __( 'Error - insufficient permissions!', 'mainwp' ) ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Import handlers escape the message when sending the response.
2472 + }
2070 2473 // Check if client does not exist then create new one.
2071 2474 $client = MainWP_DB_Client::instance()->get_wp_client_by( 'client_email', $client_data['client_email'], OBJECT );
2475 + if ( $client && ! MainWP_Client::has_client_access( $client->client_id ) ) {
2476 + throw new MainWP_Exception( __( 'Error - insufficient permissions!', 'mainwp' ) ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Import handlers escape the message when sending the response.
2477 + }
2072 2478 if ( empty( $client ) ) {
2479 + $this->validate_client_import_columns( $client_data['name'], $client_data['client_email'] );
2073 2480 $client = MainWP_DB_Client::instance()->update_client( $client_data ); // Create client.
2481 + if ( ! $client ) {
2482 + throw new MainWP_Exception( __( 'A client could not be saved, so the import stopped. Clients before it in the list may have been added; check the Clients page.', 'mainwp' ) ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Import handlers escape the message when sending the response.
2483 + }
2074 2484 }
2075 2485
2076 2486 return $client;
2077 2487 }
@@ -2085,11 +2495,17 @@
2085 2495 * @uses MainWP_DB_Client::instance()->update_selected_sites_for_client()
2086 2496 *
2087 2497 * @param object $client client data.
2088 2498 * @param int $website_id id.
2499 + * @throws MainWP_Exception If the caller cannot attach the submitted site.
2089 2500 */
2090 2501 public function mainwp_handle_selected_sites_for_client( $client, $website_id ) {
2091 - $sites_ids = MainWP_DB_Client::instance()->get_websites_by_client_ids( $client->client_id );
2502 + $allowed = MainWP_Abilities_Util::get_allowed_site_ids();
2503 + if ( ! \mainwp_current_user_can( 'dashboard', 'manage_clients' ) || ( 'all' !== $allowed && ! in_array( (int) $website_id, $allowed, true ) ) ) {
2504 + throw new MainWP_Exception( __( 'Error - insufficient permissions!', 'mainwp' ) ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Import handlers escape the message when sending the response.
2505 + }
2506 + // Imports append to the complete roster so hidden memberships survive.
2507 + $sites_ids = MainWP_DB_Client::instance()->get_websites_by_client_ids( $client->client_id, array( 'no_perm_check' => true ) );
2092 2508 $ids = isset( $sites_ids ) && ! empty( $sites_ids ) ? array_column( $sites_ids, 'id' ) : array();
2093 2509 $ids[] = $website_id;
2094 2510 MainWP_DB_Client::instance()->update_selected_sites_for_client( $client->client_id, $ids );
2095 2511 }