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 | pages/page-mainwp-client.php +345 -81 6.1.6 → 6.3 View file →
@@ -30,8 +30,78 @@
30 30 return __CLASS__;
31 31 }
32 32
33 33 /**
34 + * Check UI scope without the REST session capability requirement.
35 + *
36 + * @param int $client_id Client ID.
37 + * @param bool $full Require access to every member site.
38 + * @return bool Whether the client is in scope.
39 + */
40 + public static function has_client_access( $client_id, $full = false ) {
41 + $allowed = MainWP_Abilities_Util::get_allowed_site_ids();
42 + if ( 'all' === $allowed ) {
43 + return true;
44 + }
45 + if ( (int) $client_id <= 0 ) {
46 + return false;
47 + }
48 + $sites = MainWP_DB_Client::instance()->get_websites_by_client_ids( (int) $client_id, array( 'no_perm_check' => true ) );
49 + if ( ! is_array( $sites ) ) {
50 + return false;
51 + }
52 + $ids = array_map( 'intval', wp_list_pluck( $sites, 'id' ) );
53 + if ( empty( $ids ) || ! array_intersect( $ids, $allowed ) ) {
54 + return false;
55 + }
56 + return ! $full || ! array_diff( $ids, $allowed );
57 + }
58 +
59 + /**
60 + * Keep only the clients a restricted user may view: at least one of the client's sites is allowed.
61 + *
62 + * Judged on the site rows from one batched roster query, never on the GROUP_CONCAT selected_sites
63 + * column: MySQL truncates that string at group_concat_max_len, and a cut-off id fragment can match
64 + * an allowed site the client does not have.
65 + *
66 + * @param mixed $clients Client rows, arrays or objects carrying client_id.
67 + * @return mixed The input unchanged for unrestricted users or non-arrays, otherwise the visible rows reindexed.
68 + */
69 + public static function filter_viewable_clients( $clients ) {
70 + $allowed = MainWP_Abilities_Util::get_allowed_site_ids();
71 + if ( 'all' === $allowed || ! is_array( $clients ) ) {
72 + return $clients;
73 + }
74 +
75 + $client_id = static function ( $client ) {
76 + if ( is_array( $client ) ) {
77 + return isset( $client['client_id'] ) ? (int) $client['client_id'] : 0;
78 + }
79 + return is_object( $client ) && isset( $client->client_id ) ? (int) $client->client_id : 0;
80 + };
81 +
82 + $visible = array();
83 + $ids = array_filter( array_unique( array_map( $client_id, $clients ) ) );
84 + if ( ! empty( $ids ) && ! empty( $allowed ) ) {
85 + $sites = MainWP_DB_Client::instance()->get_websites_by_client_ids( array_values( $ids ), array( 'no_perm_check' => true ) );
86 + foreach ( (array) $sites as $site ) {
87 + if ( in_array( (int) $site->id, $allowed, true ) ) {
88 + $visible[ (int) $site->client_id ] = true;
89 + }
90 + }
91 + }
92 +
93 + return array_values(
94 + array_filter(
95 + $clients,
96 + static function ( $client ) use ( $client_id, $visible ) {
97 + return isset( $visible[ $client_id( $client ) ] );
98 + }
99 + )
100 + );
101 + }
102 +
103 + /**
34 104 * Current page.
35 105 *
36 106 * @static
37 107 * @var string $page Current page.
@@ -128,9 +198,9 @@
128 198 add_submenu_page(
129 199 'mainwp_tab',
130 200 esc_html__( 'Clients', 'mainwp' ),
131 201 '<div class="mainwp-hidden">' . esc_html__( 'Client Fields', 'mainwp' ) . '</div>',
132 - 'read',
202 + 'manage_options',
133 203 'ClientAddField',
134 204 array(
135 205 static::get_class_name(),
136 206 'render_client_fields',
@@ -211,9 +281,9 @@
211 281 <?php } ?>
212 282 <?php if ( ! MainWP_Menu::is_disable_menu_item( 3, 'ClientImport' ) ) { ?>
213 283 <a href="<?php echo esc_url( admin_url( 'admin.php?page=ClientImport' ) ); ?>" class="mainwp-submenu"><?php esc_html_e( 'Import Clients', 'mainwp' ); ?></a>
214 284 <?php } ?>
215 - <?php if ( ! MainWP_Menu::is_disable_menu_item( 3, 'ClientAddField' ) ) { ?>
285 + <?php if ( \mainwp_current_user_can( 'dashboard', 'manage_clients' ) && ! MainWP_Menu::is_disable_menu_item( 3, 'ClientAddField' ) ) { ?>
216 286 <a href="<?php echo esc_url( admin_url( 'admin.php?page=ClientAddField' ) ); ?>" class="mainwp-submenu"><?php esc_html_e( 'Client Properties', 'mainwp' ); ?></a>
217 287 <?php } ?>
218 288 <?php
219 289 if ( isset( static::$subPages ) && is_array( static::$subPages ) ) {
@@ -284,9 +354,9 @@
284 354 'title' => esc_html__( 'Client Fields', 'mainwp' ),
285 355 'parent_key' => 'ManageClients',
286 356 'href' => 'admin.php?page=ClientAddField',
287 357 'slug' => 'ClientAddField',
288 - 'right' => '',
358 + 'right' => 'manage_clients',
289 359 'leftsub_order_level2' => 4,
290 360 ),
291 361 );
292 362
@@ -311,8 +381,17 @@
311 381 $delete = isset( $_POST['delete'] ) ? intval( $_POST['delete'] ) : 0;
312 382 $client_id = isset( $_POST['iconItemId'] ) ? intval( $_POST['iconItemId'] ) : 0;
313 383 $delnonce = isset( $_POST['delnonce'] ) ? sanitize_key( $_POST['delnonce'] ) : '';
314 384
385 + if ( ! \mainwp_current_user_can( 'dashboard', 'manage_clients' ) || ( $client_id && ( ! static::has_client_access( $client_id ) || ! MainWP_DB_Client::instance()->get_wp_client_by( 'client_id', $client_id ) ) ) ) {
386 + static::exit_json(
387 + array(
388 + 'result' => 'failed',
389 + 'error' => esc_html__( 'Error - insufficient permissions!', 'mainwp' ),
390 + )
391 + );
392 + }
393 +
315 394 if ( $delete ) {
316 395 if ( ! MainWP_System_Utility::is_valid_custom_nonce( 'client', $iconfile_slug, $delnonce ) ) {
317 396 die( 'Invalid nonce!' );
318 397 }
@@ -323,12 +402,12 @@
323 402 'image' => '',
324 403 'client_id' => $client_id,
325 404 );
326 405 MainWP_DB_Client::instance()->update_client( $update );
327 - MainWP_Utility::instance()->delete_uploaded_icon_file( 'client-images', $client->image );
406 + MainWP_Utility::delete_client_image( $client->image );
328 407 }
329 408 } elseif ( ! empty( $iconfile_slug ) ) {
330 - MainWP_Utility::instance()->delete_uploaded_icon_file( 'client-images', $iconfile_slug );
409 + MainWP_Utility::delete_client_image( $iconfile_slug );
331 410 }
332 411 wp_die( wp_json_encode( array( 'result' => 'success' ) ) );
333 412 }
334 413
@@ -335,9 +414,9 @@
335 414 $output = isset( $_FILES['mainwp_upload_icon_uploader'] ) ? MainWP_System_Utility::handle_upload_image( 'client-images', $_FILES['mainwp_upload_icon_uploader'] ) : null;
336 415 // phpcs:enable WordPress.Security.NonceVerification,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
337 416
338 417 $uploaded_icon = 'NOTCHANGE';
339 - if ( is_array( $output ) && isset( $output['filename'] ) && ! empty( $output['filename'] ) ) {
418 + if ( is_array( $output ) && isset( $output['filename'] ) && MainWP_Utility::is_valid_client_image_name( $output['filename'] ) ) {
340 419 $uploaded_icon = $output['filename'];
341 420 }
342 421
343 422 if ( 'NOTCHANGE' !== $uploaded_icon ) {
@@ -377,8 +456,18 @@
377 456 $delete = isset( $_POST['delete'] ) ? intval( $_POST['delete'] ) : 0;
378 457 $contact_id = isset( $_POST['iconItemId'] ) ? intval( $_POST['iconItemId'] ) : 0;
379 458 $delnonce = isset( $_POST['delnonce'] ) ? sanitize_key( $_POST['delnonce'] ) : '';
380 459
460 + $contact = $contact_id ? MainWP_DB_Client::instance()->get_wp_client_contact_by( 'contact_id', $contact_id ) : null;
461 + if ( ! \mainwp_current_user_can( 'dashboard', 'manage_clients' ) || ( $contact_id && ( ! $contact || ! static::has_client_access( $contact->contact_client_id ) ) ) ) {
462 + static::exit_json(
463 + array(
464 + 'result' => 'failed',
465 + 'error' => esc_html__( 'Error - insufficient permissions!', 'mainwp' ),
466 + )
467 + );
468 + }
469 +
381 470 if ( $delete ) {
382 471 if ( ! MainWP_System_Utility::is_valid_custom_nonce( 'contact', $iconfile_slug, $delnonce ) ) {
383 472 die( 'Invalid nonce!' );
384 473 }
@@ -389,12 +478,12 @@
389 478 'contact_image' => '',
390 479 'contact_id' => $contact_id,
391 480 );
392 481 MainWP_DB_Client::instance()->update_client_contact( $update );
393 - MainWP_Utility::instance()->delete_uploaded_icon_file( 'client-images', $contact_data->contact_image );
482 + MainWP_Utility::delete_client_image( $contact_data->contact_image );
394 483 }
395 484 } elseif ( ! empty( $iconfile_slug ) ) {
396 - MainWP_Utility::instance()->delete_uploaded_icon_file( 'client-images', $iconfile_slug );
485 + MainWP_Utility::delete_client_image( $iconfile_slug );
397 486 }
398 487 wp_die( wp_json_encode( array( 'result' => 'success' ) ) );
399 488 }
400 489
@@ -401,9 +490,9 @@
401 490 $output = isset( $_FILES['mainwp_upload_icon_uploader'] ) ? MainWP_System_Utility::handle_upload_image( 'client-images', $_FILES['mainwp_upload_icon_uploader'] ) : null;
402 491 // phpcs:enable WordPress.Security.NonceVerification,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
403 492
404 493 $uploaded_icon = 'NOTCHANGE';
405 - if ( is_array( $output ) && isset( $output['filename'] ) && ! empty( $output['filename'] ) ) {
494 + if ( is_array( $output ) && isset( $output['filename'] ) && MainWP_Utility::is_valid_client_image_name( $output['filename'] ) ) {
406 495 $uploaded_icon = $output['filename'];
407 496 }
408 497
409 498 if ( 'NOTCHANGE' !== $uploaded_icon ) {
@@ -447,8 +536,13 @@
447 536 public static function render_header( $shownPage = '' ) { // phpcs:ignore -- NOSONAR -Current complexity is the only way to achieve desired results, pull request solutions appreciated.
448 537 // phpcs:disable WordPress.Security.NonceVerification,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
449 538 $client_id = isset( $_GET['client_id'] ) ? intval( $_GET['client_id'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
450 539
540 + if ( $client_id && ( ( ! \mainwp_current_user_can( 'dashboard', 'manage_clients' ) && ! \mainwp_current_user_can( 'dashboard', 'access_client_dashboard' ) ) || ! static::has_client_access( $client_id ) ) ) {
541 + \mainwp_do_not_have_permissions( esc_html__( 'client dashboard', 'mainwp' ) );
542 + return;
543 + }
544 +
451 545 $params = array(
452 546 'title' => esc_html__( 'Clients', 'mainwp' ),
453 547 'which' => 'overview' === $shownPage ? 'page_clients_overview' : '',
454 548 );
@@ -458,9 +552,9 @@
458 552 $client = MainWP_DB_Client::instance()->get_wp_client_by( 'client_id', $client_id );
459 553 if ( $client ) {
460 554 $arr_client = MainWP_Utility::map_fields( $client, array( 'image', 'selected_icon_info' ), false ); // array map.
461 555 $client_pic = MainWP_Client_Handler::get_client_contact_image( $arr_client );
462 - $params['title'] = $client_pic . '<div class="content">' . $client->name . '<div class="sub header"><a href="mailto:' . esc_attr( $client->client_email ) . '" target="_blank" style="font-weight:normal!important;">' . $client->client_email . '</a> </div></div>';
556 + $params['title'] = $client_pic . '<div class="content">' . esc_html( $client->name ) . '<div class="sub header"><a href="' . esc_url( 'mailto:' . $client->client_email ) . '" target="_blank" style="font-weight:normal!important;">' . esc_html( $client->client_email ) . '</a> </div></div>';
463 557 }
464 558 }
465 559
466 560 MainWP_UI::render_top_header( $params );
@@ -652,9 +746,9 @@
652 746 <div id="mainwp-edit-clients-modal" class="ui modal">
653 747 <i class="close icon"></i>
654 748 <div class="header"><?php esc_html_e( 'Edit client', 'mainwp' ); ?></div>
655 749 <div class="scrolling content">
656 - <div class="ui info message"><?php esc_html_e( 'Empty fields will not be passed to child sites.', 'mainwp' ); ?></div>
750 + <div class="ui info message"><?php esc_html_e( 'Blank fields will not be changed.', 'mainwp' ); ?></div>
657 751 <form id="update_client_profile" class="ui form">
658 752 <?php MainWP_UI::generate_wp_nonce( 'mainwp-admin-nonce' ); ?>
659 753 <h3 class="ui header"><?php esc_html_e( 'Name', 'mainwp' ); ?></h3>
660 754 <div class="ui grid field">
@@ -900,8 +994,12 @@
900 994 /**
901 995 * Renders the Add New Client form.
902 996 */
903 997 public static function render_add_client() {
998 + if ( ! \mainwp_current_user_can( 'dashboard', 'manage_clients' ) ) {
999 + \mainwp_do_not_have_permissions( esc_html__( 'manage clients', 'mainwp' ) );
1000 + return;
1001 + }
904 1002 $show = 'Add';
905 1003 $client_id = 0;
906 1004 $selected_sites = array();
907 1005 $edit_client = false;
@@ -906,11 +1004,19 @@
906 1004 $selected_sites = array();
907 1005 $edit_client = false;
908 1006
909 1007 if ( isset( $_GET['client_id'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.NonceVerification.Recommended
910 - $show = 'Edit';
911 - $client_id = intval( $_GET['client_id'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.NonceVerification.Recommended
912 - $edit_client = $client_id ? MainWP_DB_Client::instance()->get_wp_client_by( 'client_id', $client_id ) : false;
1008 + $show = 'Edit';
1009 + $client_id = intval( $_GET['client_id'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.NonceVerification.Recommended
1010 + if ( ! static::has_client_access( $client_id ) ) {
1011 + \mainwp_do_not_have_permissions( esc_html__( 'manage clients', 'mainwp' ) );
1012 + return;
1013 + }
1014 + $edit_client = $client_id ? MainWP_DB_Client::instance()->get_wp_client_by( 'client_id', $client_id ) : false;
1015 + if ( ! $edit_client ) {
1016 + \mainwp_do_not_have_permissions( esc_html__( 'manage clients', 'mainwp' ) );
1017 + return;
1018 + }
913 1019 $client_sites = MainWP_DB_Client::instance()->get_websites_by_client_ids( $client_id );
914 1020
915 1021 if ( $client_sites ) {
916 1022 foreach ( $client_sites as $site ) {
@@ -989,8 +1095,12 @@
989 1095 /**
990 1096 * Renders the Import Client form.
991 1097 */
992 1098 public static function render_import_clients() { // phpcs:ignore -- NOSONAR - Current complexity is the only way to achieve desired results, pull request solutions appreciated.
1099 + if ( ! \mainwp_current_user_can( 'dashboard', 'manage_clients' ) ) {
1100 + \mainwp_do_not_have_permissions( esc_html__( 'manage clients', 'mainwp' ) );
1101 + return;
1102 + }
993 1103 static::render_header( 'ImportClients' );
994 1104 $title_page = esc_html__( 'Import Clients', 'mainwp' );
995 1105 // phpcs:disable WordPress.Security.NonceVerification.Missing
996 1106
@@ -996,15 +1106,22 @@
996 1106
997 1107 $has_import_data = ! empty( $_POST['mainwp_client_import_add'] );
998 1108 // phpcs:enable WordPress.Security.NonceVerification.Missing
999 1109 if ( $has_import_data && check_admin_referer( 'mainwp-admin-nonce' ) ) {
1000 - static::render_import_client_modal();
1110 + $result = static::render_import_client_modal();
1001 1111 }
1002 1112 ?>
1003 1113 <div class="ui padded segment" id="mainwp-import-clients">
1004 1114 <form method="POST" action="" enctype="multipart/form-data" id="mainwp_client_import_form" class="ui form">
1005 1115 <div>
1006 - <?php $el_id_mes_zn_1 = 'mainwp-message-zone'; ?>
1116 + <?php
1117 + if ( ! empty( $result ) && is_wp_error( $result ) ) {
1118 + ?>
1119 + <div class="ui message yellow"><i class="close icon"></i> <?php echo esc_html( $result->get_error_message() ); ?></div>
1120 + <?php
1121 + }
1122 + $el_id_mes_zn_1 = 'mainwp-message-zone';
1123 + ?>
1007 1124 <div id="<?php echo esc_attr( $el_id_mes_zn_1 ); ?>" class="ui message" style="display:none"></div>
1008 1125 <h3 class="ui dividing header">
1009 1126 <?php echo esc_html( $title_page ); ?>
1010 1127 <div class="sub header"><?php esc_html_e( 'Import multiple clients to your MainWP Dashboard.', 'mainwp' ); ?></div>
@@ -1038,16 +1155,29 @@
1038 1155 /**
1039 1156 * Method render_import_client_modal()
1040 1157 *
1041 1158 * Render HTML import client modal.
1159 + *
1160 + * @return mixed Result.
1042 1161 */
1043 1162 public static function render_import_client_modal() {
1163 + $import_client_data = false;
1164 + $has_file_upload = isset( $_FILES['mainwp_client_import_file_bulkupload'] ) && isset( $_FILES['mainwp_client_import_file_bulkupload']['error'] ) && UPLOAD_ERR_OK === $_FILES['mainwp_client_import_file_bulkupload']['error']; // phpcs:ignore -- NOSONAR
1165 + if ( $has_file_upload ) {
1166 + $import_client_data = static::handle_client_import_files();
1167 + }
1168 + if ( ! is_array( $import_client_data ) || empty( $import_client_data['data'] ) || ! is_array( $import_client_data['data'] ) ) {
1169 + return new \WP_Error(
1170 + 'client_import_failed',
1171 + __( 'Failed to import client data. Please try again.', 'mainwp' )
1172 + );
1173 + }
1044 1174 ?>
1045 1175 <div class="ui large modal mainwp-qsw-import-client-modal" id="mainwp-import-client-modal" >
1046 1176 <i class="close icon"></i>
1047 1177 <div class="header"><?php esc_html_e( 'Import Clients', 'mainwp' ); ?></div>
1048 1178 <div class="scrolling content">
1049 - <?php static::render_import_client_row_modal(); ?>
1179 + <?php static::render_import_client_row_modal( $import_client_data ); ?>
1050 1180 </div>
1051 1181 <div class="actions">
1052 1182 <div class="ui two column grid">
1053 1183 <div class="left aligned middle aligned column">
@@ -1067,8 +1197,10 @@
1067 1197 } ).modal( 'show' );
1068 1198 } );
1069 1199 </script>
1070 1200 <?php
1201 +
1202 + return true;
1071 1203 }
1072 1204
1073 1205 /**
1074 1206 * Method render_import_client_row_modal()
@@ -1073,50 +1205,50 @@
1073 1205 /**
1074 1206 * Method render_import_client_row_modal()
1075 1207 *
1076 1208 * Render row HTML import client.
1209 + *
1210 + * @param array $client_data Import row data.
1211 + *
1212 + * @return mixed Result or WP_Error.
1077 1213 */
1078 - public static function render_import_client_row_modal() {
1079 - ?>
1080 - <div id="mainwp-importing-clients" class="ui active dimmer">
1081 - <div class="ui double text loader"><?php esc_html_e( 'Importing...', 'mainwp' ); ?></div>
1082 - </div>
1083 - <div class="ui message" id="mainwp-import-clients-status-message">
1084 - <i class="notched circle loading icon"></i> <?php echo esc_html__( 'Importing...', 'mainwp' ); ?>
1085 - </div>
1086 - <?php
1087 - $has_file_upload = isset( $_FILES['mainwp_client_import_file_bulkupload'] ) && isset( $_FILES['mainwp_client_import_file_bulkupload']['error'] ) && UPLOAD_ERR_OK === $_FILES['mainwp_client_import_file_bulkupload']['error']; // phpcs:ignore -- NOSONAR
1214 + public static function render_import_client_row_modal( $client_data = array() ) { // phpcs:ignore -- NOSONAR -complex.
1088 1215
1089 - if ( $has_file_upload ) {
1090 - $import_client_data = static::handle_client_import_files();
1091 - if ( ! empty( $import_client_data ) && is_array( $import_client_data ) ) {
1092 - $row = 0;
1093 - $header_line = trim( $import_client_data['header_line'] );
1094 - foreach ( $import_client_data['data'] as $val_client_data ) {
1095 - $encoded = wp_json_encode( $val_client_data );
1096 - $original = implode(
1097 - ', ',
1098 - array_map(
1099 - function ( $item ) {
1100 - return is_array( $item ) ? implode( ';', $item ) : $item;
1101 - },
1102 - $val_client_data
1103 - )
1104 - );
1105 - ?>
1106 - <input type="hidden" id="mainwp_manageclients_import_csv_line_<?php echo esc_attr( $row + 1 ); ?>" value="" encoded-data="<?php echo esc_attr( $encoded ); ?>" original="<?php echo esc_attr( $original ); ?>"/>
1107 - <?php
1108 - ++$row;
1109 - }
1216 + if ( is_array( $client_data ) && ! empty( $client_data['data'] ) && is_array( $client_data['data'] ) ) {
1217 + ?>
1218 + <div id="mainwp-importing-clients" class="ui active dimmer">
1219 + <div class="ui double text loader"><?php esc_html_e( 'Importing...', 'mainwp' ); ?></div>
1220 + </div>
1221 + <div class="ui message" id="mainwp-import-clients-status-message">
1222 + <i class="notched circle loading icon"></i> <?php echo esc_html__( 'Importing...', 'mainwp' ); ?>
1223 + </div>
1224 + <?php
1225 + $row = 0;
1226 + $header_line = trim( $client_data['header_line'] );
1227 + foreach ( $client_data['data'] as $val_client_data ) {
1228 + $encoded = wp_json_encode( $val_client_data );
1229 + $original = implode(
1230 + ', ',
1231 + array_map(
1232 + function ( $item ) {
1233 + return is_array( $item ) ? implode( ';', $item ) : $item;
1234 + },
1235 + $val_client_data
1236 + )
1237 + );
1110 1238 ?>
1111 - <input type="hidden" id="mainwp_manageclients_do_import" value="1"/>
1112 - <input type="hidden" id="mainwp_manageclients_total_import" value="<?php echo esc_attr( $row ); ?>"/>
1113 - <div class="mainwp_manageclients_import_listing" id="mainwp_manageclients_import_logging">
1114 - <span class="log ui medium text"><?php echo esc_html( $header_line ) . '<br/>'; ?></span>
1115 - </div>
1116 - <div class="mainwp_manageclients_import_listing" id="mainwp_manageclients_import_fail_logging" style="display: none;"><?php echo esc_html( $header_line ); ?> </div>
1239 + <input type="hidden" id="mainwp_manageclients_import_csv_line_<?php echo esc_attr( $row + 1 ); ?>" value="" encoded-data="<?php echo esc_attr( $encoded ); ?>" original="<?php echo esc_attr( $original ); ?>"/>
1117 1240 <?php
1241 + ++$row;
1118 1242 }
1243 + ?>
1244 + <input type="hidden" id="mainwp_manageclients_do_import" value="1"/>
1245 + <input type="hidden" id="mainwp_manageclients_total_import" value="<?php echo esc_attr( $row ); ?>"/>
1246 + <div class="mainwp_manageclients_import_listing" id="mainwp_manageclients_import_logging">
1247 + <span class="log ui medium text"><?php echo esc_html( $header_line ) . '<br/>'; ?></span>
1248 + </div>
1249 + <div class="mainwp_manageclients_import_listing" id="mainwp_manageclients_import_fail_logging" style="display: none;"><?php echo esc_html( $header_line ); ?> </div>
1250 + <?php
1119 1251 }
1120 1252 }
1121 1253
1122 1254 /**
@@ -1129,10 +1261,20 @@
1129 1261 *
1130 1262 * @return array Import data.
1131 1263 */
1132 1264 public static function handle_client_import_files() { // phpcs:ignore -- NOSONAR
1133 - $tmp_path = isset( $_FILES['mainwp_client_import_file_bulkupload']['tmp_name'] ) ? sanitize_text_field( wp_unslash( $_FILES['mainwp_client_import_file_bulkupload']['tmp_name'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- NOSONAR
1134 - MainWP_System_Utility::get_wp_file_system();
1265 +
1266 + if ( isset( $_FILES['mainwp_client_import_file_bulkupload']['error'] ) && UPLOAD_ERR_OK !== $_FILES['mainwp_client_import_file_bulkupload']['error'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- NOSONAR
1267 + return false;
1268 + }
1269 +
1270 + $tmp_path = isset( $_FILES['mainwp_client_import_file_bulkupload']['tmp_name'] ) ? $_FILES['mainwp_client_import_file_bulkupload']['tmp_name'] : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- NOSONAR
1271 +
1272 + if ( ! is_uploaded_file( $tmp_path ) ) {
1273 + return false;
1274 + }
1275 +
1276 + $hasWPFileSystem = MainWP_System_Utility::get_wp_file_system();
1135 1277 //phpcs:enable
1136 1278 /**
1137 1279 * WordPress files system object.
1138 1280 *
@@ -1139,14 +1281,23 @@
1139 1281 * @global object
1140 1282 */
1141 1283 global $wp_filesystem;
1142 1284
1143 - $content = $wp_filesystem->get_contents( $tmp_path );
1285 + $lines = array();
1286 + $header_line = '';
1144 1287
1145 - // to compatible with EOL on OSs.
1146 - $content = str_replace( "\r\n", "\r", $content );
1147 - $content = str_replace( "\n", "\r", $content );
1148 - $lines = explode( "\r", $content );
1288 + if ( $hasWPFileSystem && ! empty( $wp_filesystem ) ) {
1289 + $content = $wp_filesystem->get_contents( $tmp_path );
1290 + if ( ! empty( $content ) ) {
1291 + // to compatible with EOL on OSs.
1292 + $content = str_replace( "\r\n", "\r", $content );
1293 + $content = str_replace( "\n", "\r", $content );
1294 + $lines = explode( "\r", $content );
1295 + }
1296 + } else {
1297 + return false;
1298 + }
1299 +
1149 1300 $import_data = array();
1150 1301 $default_values = array(
1151 1302 'client.name' => '',
1152 1303 'client.email' => '',
@@ -1206,8 +1357,13 @@
1206 1357 * Renders the Add New Client Fields form.
1207 1358 */
1208 1359 public static function render_client_fields() { // phpcs:ignore -- NOSONAR - Current complexity is the only way to achieve desired results, pull request solutions appreciated.
1209 1360
1361 + if ( ! \mainwp_current_user_can( 'dashboard', 'manage_clients' ) ) {
1362 + \mainwp_do_not_have_permissions( esc_html__( 'manage clients', 'mainwp' ) );
1363 + return;
1364 + }
1365 +
1210 1366 $fields = MainWP_DB_Client::instance()->get_client_fields();
1211 1367 $has_no_fields = empty( $fields );
1212 1368
1213 1369 static::render_header( 'AddField' );
@@ -1371,8 +1527,13 @@
1371 1527 * Bulk client addition $_POST Handler.
1372 1528 */
1373 1529 public static function add_client() { // phpcs:ignore -- NOSONAR -Current complexity is required to achieve desired results. Pull request solutions appreciated.
1374 1530
1531 + if ( ! \mainwp_current_user_can( 'dashboard', 'manage_clients' ) ) {
1532 + echo wp_json_encode( array( 'error' => esc_html__( 'Error - insufficient permissions!', 'mainwp' ) ) );
1533 + return;
1534 + }
1535 +
1375 1536 $selected_sites = ( isset( $_POST['selected_sites'] ) && is_array( $_POST['selected_sites'] ) ) ? array_map( 'sanitize_text_field', wp_unslash( $_POST['selected_sites'] ) ) : array(); //phpcs:ignore WordPress.Security.NonceVerification.Missing
1376 1537 $client_fields = isset( $_POST['client_fields'] ) ? wp_unslash( $_POST['client_fields'] ) : array(); //phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1377 1538
1378 1539 if ( ! is_array( $client_fields ) ) {
@@ -1384,13 +1545,60 @@
1384 1545 if ( ! empty( $editing_client_id ) && ! wp_verify_nonce( $_POST['nonce_client_id'], 'editing-client-' . $editing_client_id ) ) { //phpcs:ignore -- NOSONAR - ok.
1385 1546 die( 'Invalid nonce!' );
1386 1547 }
1387 1548
1549 + $current_client = $editing_client_id ? MainWP_DB_Client::instance()->get_wp_client_by( 'client_id', $editing_client_id ) : null;
1550 + $allowed_sites = MainWP_Abilities_Util::get_allowed_site_ids();
1551 + if ( ( $editing_client_id && ( ! $current_client || ! static::has_client_access( $editing_client_id ) ) ) || ( 'all' !== $allowed_sites && array_diff( array_map( 'intval', $selected_sites ), $allowed_sites ) ) ) {
1552 + echo wp_json_encode( array( 'error' => esc_html__( 'Error - insufficient permissions!', 'mainwp' ) ) );
1553 + return;
1554 + }
1555 +
1556 + // Foreign contacts and fields must not authorize a partial client save.
1557 + $primary_id = isset( $client_fields['default_field']['primary_contact_id'] ) ? intval( $client_fields['default_field']['primary_contact_id'] ) : 0;
1558 + $primary = $primary_id ? MainWP_DB_Client::instance()->get_wp_client_contact_by( 'contact_id', $primary_id ) : null;
1559 + if ( $primary_id && ( ! $primary || ! $editing_client_id || (int) $primary->contact_client_id !== $editing_client_id ) ) {
1560 + echo wp_json_encode( array( 'error' => esc_html__( 'Error - insufficient permissions!', 'mainwp' ) ) );
1561 + return;
1562 + }
1563 + foreach ( isset( $client_fields['custom_fields'] ) && is_array( $client_fields['custom_fields'] ) ? $client_fields['custom_fields'] : array() as $field_val ) {
1564 + $field_id = is_array( $field_val ) ? array_key_first( $field_val ) : 0;
1565 + $field = $field_id ? MainWP_DB_Client::instance()->get_client_fields_by( 'field_id', $field_id ) : null;
1566 + if ( ! $field || ( (int) $field->client_id && ( ! $editing_client_id || (int) $field->client_id !== $editing_client_id ) ) ) {
1567 + echo wp_json_encode( array( 'error' => esc_html__( 'Error - insufficient permissions!', 'mainwp' ) ) );
1568 + return;
1569 + }
1570 + }
1571 + $contact_ids = isset( $client_fields['contacts_field']['contact_id'] ) && is_array( $client_fields['contacts_field']['contact_id'] ) ? $client_fields['contacts_field']['contact_id'] : array();
1572 + if ( isset( $client_fields['delele_contacts'] ) && is_array( $client_fields['delele_contacts'] ) ) {
1573 + $contact_ids = array_merge( $contact_ids, $client_fields['delele_contacts'] );
1574 + }
1575 + foreach ( $contact_ids as $contact_id ) {
1576 + if ( ! $contact_id ) {
1577 + continue;
1578 + }
1579 + $contact = MainWP_DB_Client::instance()->get_wp_client_contact_by( 'contact_id', intval( $contact_id ) );
1580 + if ( ! $contact || ! $editing_client_id || (int) $contact->contact_client_id !== $editing_client_id ) {
1581 + echo wp_json_encode( array( 'error' => esc_html__( 'Error - insufficient permissions!', 'mainwp' ) ) );
1582 + return;
1583 + }
1584 + }
1585 + if ( $current_client && isset( $client_fields['default_field']['client.suspended'] ) && (int) $client_fields['default_field']['client.suspended'] !== (int) $current_client->suspended && ! static::has_client_access( $editing_client_id, true ) ) {
1586 + echo wp_json_encode( array( 'error' => esc_html__( 'Error - insufficient permissions!', 'mainwp' ) ) );
1587 + return;
1588 + }
1589 +
1388 1590 if ( ! isset( $client_fields['default_field']['client.name'] ) || empty( $client_fields['default_field']['client.name'] ) ) {
1389 1591 echo wp_json_encode( array( 'error' => esc_html__( 'Client name are empty. Please try again.', 'mainwp' ) ) );
1390 1592 return;
1391 1593 }
1392 1594
1595 + $posted_note = isset( $client_fields['default_field']['client.note'] ) ? $client_fields['default_field']['client.note'] : null;
1596 + if ( MainWP_Utility::client_note_exceeds_limit( $posted_note ) ) {
1597 + echo wp_json_encode( array( 'error' => MainWP_Utility::client_note_length_error() ) );
1598 + return;
1599 + }
1600 +
1393 1601 $add_new = true;
1394 1602
1395 1603 $default_client_fields = MainWP_Client_Handler::get_default_client_fields();
1396 1604 $client_to_add = array();
@@ -1395,9 +1603,16 @@
1395 1603 $default_client_fields = MainWP_Client_Handler::get_default_client_fields();
1396 1604 $client_to_add = array();
1397 1605 foreach ( $default_client_fields as $field_name => $item ) {
1398 1606 if ( ! empty( $item['db_field'] ) && isset( $client_fields['default_field'][ $field_name ] ) ) {
1399 - $client_to_add[ $item['db_field'] ] = sanitize_text_field( wp_unslash( $client_fields['default_field'][ $field_name ] ) );
1607 + if ( 'note' === $item['db_field'] ) {
1608 + // client_fields is already unslashed. A non-string note is stored as empty.
1609 + $field_value = $client_fields['default_field'][ $field_name ];
1610 + $client_to_add[ $item['db_field'] ] = is_string( $field_value ) ? MainWP_Utility::sanitize_client_note( $field_value ) : '';
1611 + } else {
1612 + $field_value = wp_unslash( $client_fields['default_field'][ $field_name ] );
1613 + $client_to_add[ $item['db_field'] ] = sanitize_text_field( $field_value );
1614 + }
1400 1615 }
1401 1616 }
1402 1617
1403 1618 $client_to_add['primary_contact_id'] = isset( $client_fields['default_field']['primary_contact_id'] ) ? intval( $client_fields['default_field']['primary_contact_id'] ) : 0;
@@ -1407,10 +1622,9 @@
1407 1622 $new_suspended = isset( $client_to_add['suspended'] ) ? (int) $client_to_add['suspended'] : 0;
1408 1623 $old_suspended = $new_suspended;
1409 1624
1410 1625 if ( $client_id ) {
1411 - $current_client = MainWP_DB_Client::instance()->get_wp_client_by( 'client_id', $client_id );
1412 - $old_suspended = $current_client->suspended;
1626 + $old_suspended = $current_client->suspended;
1413 1627
1414 1628 $client_to_add['client_id'] = $client_id; // update client.
1415 1629 if ( isset( $client_to_add['created'] ) && ! empty( $client_to_add['created'] ) ) {
1416 1630 $client_to_add['created'] = strtotime( $client_to_add['created'] );
@@ -1495,20 +1709,24 @@
1495 1709 $client_image = sanitize_text_field( wp_unslash( $_POST['mainwp_add_edit_client_uploaded_icon_hidden'] ) ); // phpcs:ignore WordPress.Security.NonceVerification
1496 1710 }
1497 1711
1498 1712 // compatible with quick setup.
1499 - if ( isset( $_FILES['mainwp_client_image_uploader'] ) && isset( $_FILES['mainwp_client_image_uploader']['error']['client_field'] ) && UPLOAD_ERR_OK === $_FILES['mainwp_client_image_uploader']['error']['client_field'] ) { // phpcs:ignore WordPress.Security.NonceVerification
1500 - $output = MainWP_System_Utility::handle_upload_image( 'client-images', $_FILES['mainwp_client_image_uploader'], 'client_field' ); // phpcs:ignore WordPress.Security.NonceVerification,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1501 - if ( is_array( $output ) && isset( $output['filename'] ) && ! empty( $output['filename'] ) ) {
1713 + if ( isset( $_FILES['mainwp_client_image_uploader']['error']['client_field'] ) && UPLOAD_ERR_OK === $_FILES['mainwp_client_image_uploader']['error']['client_field'] ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1714 + $output = MainWP_System_Utility::handle_upload_image( 'client-images', $_FILES['mainwp_client_image_uploader'], 'client_field' ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1715 + if ( is_array( $output ) && isset( $output['filename'] ) && MainWP_Utility::is_valid_client_image_name( $output['filename'] ) ) {
1502 1716 $client_image = $output['filename'];
1503 1717 }
1504 1718 }
1505 1719
1506 1720 $client_data = MainWP_DB_Client::instance()->get_wp_client_by( 'client_id', $client_id );
1721 + if ( $client_data && ! MainWP_Utility::is_valid_client_image_name( $client_image ) ) {
1722 + $client_image = $client_data->image;
1723 + }
1507 1724 if ( $client_data && $client_data->image !== $client_image && $client_id ) {
1725 + // The hidden field is attacker-controlled; only the stored name identifies the old image.
1508 1726 $old_file = $client_data->image;
1509 1727 if ( $old_file !== $client_image && ! empty( $old_file ) ) {
1510 - MainWP_Utility::instance()->delete_uploaded_icon_file( 'client-images', $old_file );
1728 + MainWP_Utility::delete_client_image( $old_file );
1511 1729 }
1512 1730 $update = array(
1513 1731 'client_id' => $client_id,
1514 1732 'image' => $client_image,
@@ -1582,11 +1800,12 @@
1582 1800 if ( isset( $_POST['mainwp_add_edit_contact_uploaded_icon_hidden']['contacts_field'][ $indx ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
1583 1801 $contact_image = sanitize_text_field( wp_unslash( $_POST['mainwp_add_edit_contact_uploaded_icon_hidden']['contacts_field'][ $indx ] ) ); // phpcs:ignore WordPress.Security.NonceVerification
1584 1802 }
1585 1803 if ( $contact_data && $contact_data->contact_image !== $contact_image && $contact_id ) {
1804 + // The hidden field is attacker-controlled; only the stored name identifies the old image.
1586 1805 $old_file = $contact_data->contact_image;
1587 1806 if ( $old_file !== $contact_image && ! empty( $old_file ) ) {
1588 - MainWP_Utility::instance()->delete_uploaded_icon_file( 'client-images', $old_file );
1807 + MainWP_Utility::delete_client_image( $old_file );
1589 1808 }
1590 1809 $update = array(
1591 1810 'contact_id' => $contact_id,
1592 1811 'contact_image' => $contact_image,
@@ -2036,9 +2255,9 @@
2036 2255 if ( 'client.note' === $field_name ) {
2037 2256 $el_id_fln_1 = $field_name;
2038 2257 ?>
2039 2258 <div class="editor">
2040 - <textarea class="code settings-field-value-change-handler" cols="80" rows="10" id="client_fields[default_field][<?php echo esc_attr( $el_id_fln_1 ); ?>]" name="client_fields[default_field][<?php echo esc_attr( $field_name ); ?>]"><?php echo esc_html( $val ); ?></textarea>
2259 + <textarea class="code settings-field-value-change-handler" cols="80" rows="10" id="client_fields[default_field][<?php echo esc_attr( $el_id_fln_1 ); ?>]" name="client_fields[default_field][<?php echo esc_attr( $field_name ); ?>]"><?php echo esc_textarea( MainWP_Utility::client_note_for_editor( (string) $val ) ); ?></textarea>
2041 2260 </div>
2042 2261 <?php
2043 2262 } elseif ( $client_id && 'client.created' === $field_name ) {
2044 2263 $created = empty( $val ) ? time() : $val;
@@ -2163,8 +2382,11 @@
2163 2382 </div>
2164 2383 <div class="ui section hidden divider after-add-contact-field"></div>
2165 2384 </div>
2166 2385 </div>
2386 + <?php if ( $edit_client ) : ?>
2387 + <?php MainWP_Knowledge_UI::render_panel( 'client', $client_id, true ); ?>
2388 + <?php endif; ?>
2167 2389 </div>
2168 2390 <input type="hidden" name="client_fields[client_id]" value="<?php echo intval( $client_id ); ?>">
2169 2391 <script type="text/javascript">
2170 2392 jQuery( document ).ready( function () {
@@ -2533,8 +2755,21 @@
2533 2755 $field_name = isset( $_POST['field_name'] ) ? sanitize_text_field( wp_unslash( $_POST['field_name'] ) ) : '';
2534 2756 $field_name = trim( $field_name, '[]' );
2535 2757 // phpcs:enable
2536 2758
2759 + if ( $field_id ) {
2760 + $owner = MainWP_DB_Client::instance()->get_client_fields_by( 'field_id', $field_id );
2761 + if ( ! $owner ) {
2762 + $return['error'] = esc_html__( 'Error - insufficient permissions!', 'mainwp' );
2763 + static::exit_json( $return );
2764 + }
2765 + $client_id = (int) $owner->client_id;
2766 + }
2767 + if ( ! \mainwp_current_user_can( 'dashboard', 'manage_clients' ) || ( $client_id && ( ! static::has_client_access( $client_id ) || ! MainWP_DB_Client::instance()->get_wp_client_by( 'client_id', $client_id ) ) ) ) {
2768 + $return['error'] = esc_html__( 'Error - insufficient permissions!', 'mainwp' );
2769 + static::exit_json( $return );
2770 + }
2771 +
2537 2772 // update general or individual client field.
2538 2773 if ( $field_id ) {
2539 2774 $current = MainWP_DB_Client::instance()->get_client_fields_by( 'field_id', $field_id );
2540 2775 if ( $current && $current->field_name === $field_name && $current->field_desc === $field_desc ) {
@@ -2590,23 +2825,52 @@
2590 2825 * Save Client Note.
2591 2826 */
2592 2827 public static function save_note() {
2593 2828 if ( isset( $_POST['clientid'] ) && ! empty( $_POST['clientid'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
2594 - $note = isset( $_POST['note'] ) ? wp_unslash( $_POST['note'] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
2595 - $esc_note = MainWP_Utility::esc_content( $note );
2596 - $update = array(
2829 + $note = isset( $_POST['note'] ) ? wp_unslash( $_POST['note'] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
2830 + if ( ! is_string( $note ) ) {
2831 + $note = '';
2832 + }
2833 + if ( MainWP_Utility::client_note_exceeds_limit( $note ) ) {
2834 + self::exit_json(
2835 + array(
2836 + 'error' => MainWP_Utility::client_note_length_error(),
2837 + )
2838 + );
2839 + }
2840 + $stored = MainWP_Utility::sanitize_client_note( $note );
2841 + $update = array(
2597 2842 'client_id' => intval( $_POST['clientid'] ), // phpcs:ignore WordPress.Security.NonceVerification,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
2598 - 'note' => $esc_note,
2843 + 'note' => $stored,
2599 2844 );
2600 2845 MainWP_DB_Client::instance()->update_client( $update );
2601 - die(
2602 - wp_json_encode(
2603 - array(
2604 - 'result' => 'SUCCESS',
2605 - 'esc_note_content' => $esc_note,
2606 - )
2846 + self::exit_json(
2847 + array(
2848 + 'result' => 'SUCCESS',
2849 + // The editor shows < and >, not the stored &lt; / &gt; text.
2850 + 'note' => MainWP_Utility::client_note_for_editor( $stored ),
2851 + 'esc_note_content' => MainWP_Utility::client_note_html( $stored ),
2607 2852 )
2608 2853 );
2609 2854 }
2610 - die( wp_json_encode( array( 'undefined_error' => true ) ) );
2855 + self::exit_json( array( 'undefined_error' => true ) );
2856 + }
2857 +
2858 + /**
2859 + * Send a JSON payload and stop the request.
2860 + *
2861 + * During an AJAX request this uses wp_die() so the status stays HTTP 200
2862 + * and the body stays the JSON string. Otherwise it dies with that string.
2863 + *
2864 + * Shared by the notes modal and the clients notes AJAX handler.
2865 + *
2866 + * @param array $payload Response data.
2867 + * @return void
2868 + */
2869 + public static function exit_json( $payload ) {
2870 + $json = wp_json_encode( $payload );
2871 + if ( function_exists( 'wp_doing_ajax' ) && wp_doing_ajax() ) {
2872 + wp_die( $json ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- JSON response body.
2873 + }
2874 + die( $json ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- JSON response body.
2611 2875 }
2612 2876 }