| @@ -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 | } |