PluginProbe
UsersWP – Front-end login form, User Registration, User Profile & Members Directory plugin for WP / 1.2.74
UsersWP – Front-end login form, User Registration, User Profile & Members Directory plugin for WP v1.2.74
1.2.74 1.2.73 1.2.72 1.2.71 1.2.70 1.2.69 1.2.68 1.2.67 1.2.66 1.2.65 1.2.64 1.2.63 trunk 1.0.10 1.0.11 1.0.12 1.0.13 1.0.14 1.0.15 1.0.16 1.0.17 1.0.18 1.0.19 1.0.20 1.0.21 All 174 releases
← All changes | includes/class-import-export.php +403 -79 1.0.171.2.74 View file →
@@ -16,9 +16,52 @@
16 16 public $path;
17 17 public $total_rows;
18 18 public $imp_step;
19 19 public $skipped;
20 + private $empty;
21 + private $step;
22 + private $file;
23 + private $filename;
20 24
25 + /**
26 + * Columns that are NEVER writable via CSV import.
27 + * Denylist wins over allowlist — a column here can never be accidentally
28 + * re-enabled by adding it to $import_meta_allowlist.
29 + *
30 + * @var array
31 + */
32 + private static $import_meta_denylist = array(
33 + 'user_id', // Primary key — never importable.
34 + 'old_password', // Credential-adjacent — must not be set via import.
35 + );
36 +
37 + /**
38 + * Columns that ARE permitted in a CSV import (positive / allowlist).
39 + * Everything not listed here is silently skipped — default-deny.
40 + * Add new safe meta keys here deliberately; never use a wildcard.
41 + *
42 + * @var array
43 + */
44 + private static $import_meta_allowlist = array(
45 + // Core WP user fields
46 + 'username',
47 + 'email',
48 + 'display_name',
49 + 'first_name',
50 + 'last_name',
51 + 'description',
52 + 'user_url',
53 + 'user_registered',
54 + 'role',
55 + // uwp_usermeta safe fields
56 + 'bio',
57 + 'phone',
58 + 'user_privacy',
59 + 'avatar_thumb',
60 + 'banner_thumb',
61 + );
62 +
63 +
21 64 public function __construct() {
22 65 global $wp_filesystem;
23 66
24 67 if ( empty( $wp_filesystem ) ) {
@@ -29,49 +72,32 @@
29 72
30 73 $this->wp_filesystem = $wp_filesystem;
31 74 $this->export_dir = $this->export_location();
32 75 $this->export_url = $this->export_location( true );
33 - $this->per_page = 20;
76 + $this->per_page = apply_filters('uwp_import_export_per_page', 20, $this);
34 77 $this->meta_table_name = get_usermeta_table_prefix() . 'uwp_usermeta';
35 78 $this->path = '';
36 79
37 - add_action( 'userswp_settings_import-export_tab_content', array($this, 'get_ie_content') );
38 - add_action( 'admin_init', array($this, 'uwp_process_settings_export') );
39 - add_action( 'admin_init', array($this, 'uwp_process_settings_import') );
40 - add_action( 'wp_ajax_uwp_ajax_export_users', array( $this, 'uwp_process_users_export' ) );
41 - add_action( 'wp_ajax_uwp_ajax_import_users', array( $this, 'uwp_process_users_import' ) );
42 - add_action( 'wp_ajax_uwp_ie_upload_file', array( $this, 'uwp_ie_upload_file' ) );
43 - add_action( 'wp_ajax_nopriv_uwp_ie_upload_file', array( $this, 'uwp_ie_upload_file' ) );
44 - add_action( 'admin_notices', array($this, 'uwp_ie_admin_notice') );
45 - add_filter( 'uwp_get_export_users_status', array( $this, 'uwp_get_export_users_status' ) );
46 - add_filter( 'uwp_get_import_users_status', array( $this, 'uwp_get_import_users_status' ) );
80 + add_action( 'admin_init', array($this, 'process_settings_export') );
81 + add_action( 'admin_init', array($this, 'process_settings_import') );
82 + add_action( 'wp_ajax_uwp_ajax_export_users', array( $this, 'process_users_export' ) );
83 + add_action( 'wp_ajax_uwp_ajax_import_users', array( $this, 'process_users_import' ) );
84 + add_action( 'wp_ajax_uwp_ie_upload_file', array( $this, 'ie_upload_file' ) );
85 + add_action( 'admin_notices', array($this, 'ie_admin_notice') );
86 + add_filter( 'uwp_get_export_users_status', array( $this, 'get_export_users_status' ) );
87 + add_filter( 'uwp_get_import_users_status', array( $this, 'get_import_users_status' ) );
47 88 }
48 89
49 - public function get_ie_content() {
50 - $subtab = 'ie-users';
51 -
52 - if (isset($_GET['subtab'])) {
53 - $subtab = $_GET['subtab'];
54 - }
55 - ?>
56 - <div class="item-list-sub-tabs">
57 - <ul class="item-list-sub-tabs-ul">
58 - <li class="<?php if ($subtab == 'ie-users') { echo "current selected"; } ?>">
59 - <a href="<?php echo add_query_arg(array('tab' => 'import-export', 'subtab' => 'ie-users')); ?>"><?php echo __( 'Users', 'userswp' ); ?></a>
60 - </li>
61 - <li class="<?php if ($subtab == 'ie-settings') { echo "current selected"; } ?>">
62 - <a href="<?php echo add_query_arg(array('tab' => 'import-export', 'subtab' => 'ie-settings')); ?>"><?php echo __( 'Settings', 'userswp' ); ?></a>
63 - </li>
64 - </ul>
65 - </div>
66 - <?php
67 - if ($subtab == 'ie-users') {
68 - include_once( USERSWP_PATH . '/admin/settings/admin-settings-ie-users.php' );
69 - } elseif ($subtab == 'ie-settings') {
70 - include_once( USERSWP_PATH . '/admin/settings/admin-settings-ie-settings.php' );
71 - }
72 - }
73 -
90 + /**
91 + * Returns export file location
92 + *
93 + * @package userswp
94 + *
95 + * @param bool $relative
96 + *
97 + * @return string
98 + *
99 + */
74 100 public function export_location( $relative = false ) {
75 101 $upload_dir = wp_upload_dir();
76 102 $export_location = $relative ? trailingslashit( $upload_dir['baseurl'] ) . 'cache' : trailingslashit( $upload_dir['basedir'] ) . 'cache';
77 103 $export_location = apply_filters( 'uwp_export_location', $export_location, $relative );
@@ -78,13 +104,16 @@
78 104
79 105 return trailingslashit( $export_location );
80 106 }
81 107
82 - public function uwp_ie_admin_notice(){
108 + /**
109 + * Displays notice
110 + */
111 + public function ie_admin_notice(){
83 112 if(isset($_GET['imp-msg']) && 'success' == $_GET['imp-msg']){
84 113 ?>
85 114 <div class="notice notice-success is-dismissible">
86 - <p><?php _e( 'Settings imported successfully!', 'userswp' ); ?></p>
115 + <p><?php esc_html_e( 'Settings imported successfully!', 'userswp' ); ?></p>
87 116 </div>
88 117 <?php
89 118 }
90 119 }
@@ -91,9 +120,9 @@
91 120
92 121 /**
93 122 * Process a settings export that generates a .json file of the settings
94 123 */
95 - public function uwp_process_settings_export() {
124 + public function process_settings_export() {
96 125 if( empty( $_POST['uwp_ie_action'] ) || 'export_settings' != $_POST['uwp_ie_action'] )
97 126 return;
98 127 if( ! wp_verify_nonce( $_POST['uwp_export_nonce'], 'uwp_export_nonce' ) )
99 128 return;
@@ -111,9 +140,9 @@
111 140
112 141 /**
113 142 * Process a settings import from a json file
114 143 */
115 - public function uwp_process_settings_import() {
144 + public function process_settings_import() {
116 145 if( empty( $_POST['uwp_ie_action'] ) || 'import_settings' != $_POST['uwp_ie_action'] )
117 146 return;
118 147 if( ! wp_verify_nonce( $_POST['uwp_import_nonce'], 'uwp_import_nonce' ) )
119 148 return;
@@ -118,23 +147,27 @@
118 147 if( ! wp_verify_nonce( $_POST['uwp_import_nonce'], 'uwp_import_nonce' ) )
119 148 return;
120 149 if( ! current_user_can( 'manage_options' ) )
121 150 return;
122 - $extension = end( explode( '.', $_FILES['import_file']['name'] ) );
151 + $extension = explode( '.', $_FILES['import_file']['name'] );
152 + $extension = end( $extension );
123 153 if( $extension != 'json' ) {
124 - wp_die( sprintf(__( 'Please upload a valid .json file. %sGo Back%s' ), '<a href="'.admin_url( 'admin.php?page=userswp&tab=import-export&subtab=ie-settings' ).'">', '</a>' ));
154 + wp_die( esc_html( wp_sprintf( __( 'Please upload a valid .json file. %sGo Back%s', 'userswp' ), '<a href="' . esc_url( admin_url( 'admin.php?page=userswp&tab=import-export&section=settings' ) ) . '">', '</a>' ) ) );
125 155 }
126 156 $import_file = $_FILES['import_file']['tmp_name'];
127 157 if( empty( $import_file ) ) {
128 - wp_die( sprintf(__( 'Please upload a file to import. %sGo Back%s' ), '<a href="'.admin_url( 'admin.php?page=userswp&tab=import-export&subtab=ie-settings' ).'">', '</a>' ));
158 + wp_die( esc_html( wp_sprintf( __( 'Please upload a file to import. %sGo Back%s', 'userswp' ), '<a href="' . esc_url( admin_url( 'admin.php?page=userswp&tab=import-export&section=settings' ) ) . '">', '</a>' ) ) );
129 159 }
130 160 // Retrieve the settings from the file and convert the json object to an array.
131 161 $settings = (array) json_decode( file_get_contents( $import_file ), true );
132 162 update_option( 'uwp_settings', $settings );
133 - wp_safe_redirect( admin_url( 'admin.php?page=userswp&tab=import-export&subtab=ie-settings&imp-msg=success' ) ); exit;
163 + wp_safe_redirect( admin_url( 'admin.php?page=userswp&tab=import-export&section=settings&imp-msg=success' ) ); exit;
134 164 }
135 165
136 - public function uwp_process_users_export(){
166 + /**
167 + * Processes users export
168 + */
169 + public function process_users_export(){
137 170
138 171 $response = array();
139 172 $response['success'] = false;
140 173 $response['msg'] = __( 'Invalid export request found.', 'userswp' );
@@ -170,9 +203,9 @@
170 203 $response['msg'] = '';
171 204
172 205 if ( $done >= 100 ) {
173 206 $this->step = 'done';
174 - $new_filename = 'uwp-users-export-' . date( 'y-m-d-H-i' ) . '.csv';
207 + $new_filename = 'uwp-users-export-' . date( 'y-m-d-H-i' ) . '-' . wp_generate_password(12, false ) . '.csv';
175 208 $new_file = $this->export_dir . $new_filename;
176 209
177 210 if ( file_exists( $this->file ) ) {
178 211 $this->wp_filesystem->move( $this->file, $new_file, true );
@@ -192,17 +225,35 @@
192 225 wp_send_json( $response );
193 226
194 227 }
195 228
229 + /**
230 + * Sets export params
231 + *
232 + * @package userswp
233 + *
234 + * @param array $request
235 + *
236 + */
196 237 public function set_export_params( $request ) {
197 238 $this->empty = false;
198 239 $this->step = !empty( $request['step'] ) ? absint( $request['step'] ) : 1;
199 240 $this->filename = 'uwp-users-export-temp.csv';
200 241 $this->file = $this->export_dir . $this->filename;
242 + $chunk_per_page = !empty( $request['uwp_ie_chunk_size'] ) ? absint( $request['uwp_ie_chunk_size'] ) : 0;
243 + $this->per_page = $chunk_per_page < 50 || $chunk_per_page > 100000 ? 5000 : $chunk_per_page;
201 244
202 245 do_action( 'uwp_export_users_set_params', $request );
203 246 }
204 247
248 + /**
249 + * Returns export file location
250 + *
251 + * @package userswp
252 + *
253 + * @return bool
254 + *
255 + */
205 256 public function check_export_location() {
206 257 try {
207 258 if ( empty( $this->wp_filesystem ) ) {
208 259 return __( 'Filesystem ERROR: Could not access filesystem.', 'userswp' );
@@ -215,27 +266,35 @@
215 266 $is_dir = $this->wp_filesystem->is_dir( $this->export_dir );
216 267 $is_writeable = $is_dir && is_writeable( $this->export_dir );
217 268
218 269 if ( $is_dir && $is_writeable ) {
219 - return true;
270 +
220 271 } else if ( $is_dir && !$is_writeable ) {
221 272 if ( !$this->wp_filesystem->chmod( $this->export_dir, FS_CHMOD_DIR ) ) {
222 273 return wp_sprintf( __( 'Filesystem ERROR: Export location %s is not writable, check your file permissions.', 'userswp' ), $this->export_dir );
223 274 }
224 -
225 - return true;
226 275 } else {
227 276 if ( !$this->wp_filesystem->mkdir( $this->export_dir, FS_CHMOD_DIR ) ) {
228 277 return wp_sprintf( __( 'Filesystem ERROR: Could not create directory %s. This is usually due to inconsistent file permissions.', 'userswp' ), $this->export_dir );
229 278 }
279 + }
230 280
231 - return true;
232 - }
281 + if(!$this->wp_filesystem->exists( $this->export_dir . '/index.php')){
282 + $this->wp_filesystem->copy( USERSWP_PATH . 'assets/index.php', $this->export_dir . '/index.php' );
283 + }
284 +
285 + return true;
233 286 } catch ( Exception $e ) {
234 287 return $e->getMessage();
235 288 }
236 289 }
237 290
291 + /**
292 + * Processes export step
293 + *
294 + * @return bool
295 + *
296 + */
238 297 public function process_export_step() {
239 298 if ( $this->step < 2 ) {
240 299 @unlink( $this->file );
241 300 $this->print_columns();
@@ -249,8 +308,14 @@
249 308 return false;
250 309 }
251 310 }
252 311
312 + /**
313 + * Prints columns
314 + *
315 + * @return array
316 + *
317 + */
253 318 public function print_columns() {
254 319 $column_data = '';
255 320 $columns = $this->get_columns();
256 321 $i = 1;
@@ -265,13 +330,19 @@
265 330
266 331 return $column_data;
267 332 }
268 333
334 + /**
335 + * Returns export columns
336 + *
337 + * @return array
338 + *
339 + */
269 340 public function get_columns() {
270 341 global $wpdb;
271 342 $columns = array();
272 343
273 - foreach ( $wpdb->get_col( "DESC " . $this->meta_table_name, 0 ) as $column_name ) {
344 + foreach ( $wpdb->get_col( "DESC " . $this->meta_table_name, 0 ) as $column_name ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
274 345 $columns[] = $column_name;
275 346 }
276 347
277 348 return apply_filters( 'uwp_export_users_get_columns', $columns );
@@ -276,29 +347,66 @@
276 347
277 348 return apply_filters( 'uwp_export_users_get_columns', $columns );
278 349 }
279 350
351 + /**
352 + * Returns export data
353 + *
354 + * @package userswp
355 + *
356 + * @return array
357 + *
358 + */
280 359 public function get_export_data() {
281 360 global $wpdb;
282 - $data = $wpdb->get_results( "SELECT * FROM $this->meta_table_name WHERE 1=1 LIMIT 0,". $this->per_page);
361 + if(!$this->step){
362 + $page = 1;
363 + } else {
364 + $page = $this->step;
365 + }
283 366
367 + $page_start = absint( ( $page - 1 ) * $this->per_page );
368 + $data = $wpdb->get_results( "SELECT * FROM $this->meta_table_name WHERE 1=1 LIMIT $page_start,". $this->per_page); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
369 + $i = 0;
370 +
371 + foreach ($data as $u){
372 + $user = get_userdata($u->user_id);
373 + $data[$i]->username = $user->user_login;
374 + $data[$i]->email = $user->user_email;
375 + $data[$i]->bio = $user->description;
376 + $i++;
377 + }
378 +
284 379 return apply_filters( 'uwp_export_users_get_data', $data );
285 380 }
286 381
382 + /**
383 + * Returns export status
384 + *
385 + * @return int
386 + *
387 + */
287 388 public function get_export_status() {
288 389 $status = 100;
289 390 return apply_filters( 'uwp_get_export_users_status', $status );
290 391 }
291 392
393 + /**
394 + * Prints CSV rows
395 + *
396 + * @return string
397 + *
398 + */
292 399 public function print_rows() {
293 400 $row_data = '';
294 401 $data = $this->get_export_data();
295 402 $columns = $this->get_columns();
296 403
297 - if ( $data ) {
404 + if ( is_array($data) && !empty($data) ) {
298 405 foreach ( $data as $row ) {
299 406 $i = 1;
300 407 foreach ( $row as $key => $column ) {
408 + $column = $this->escape_data( $column );
301 409 $row_data .= '"' . addslashes( preg_replace( "/\"/","'", $column ) ) . '"';
302 410 $row_data .= $i == count( $columns ) ? '' : ',';
303 411 $i++;
304 412 }
@@ -312,8 +420,14 @@
312 420
313 421 return false;
314 422 }
315 423
424 + /**
425 + * Returns export file content
426 + *
427 + * @return string
428 + *
429 + */
316 430 protected function get_export_file() {
317 431 $file = '';
318 432
319 433 if ( $this->wp_filesystem->exists( $this->file ) ) {
@@ -324,8 +438,16 @@
324 438
325 439 return $file;
326 440 }
327 441
442 + /**
443 + * Adds export data to CSV file
444 + *
445 + * @package userswp
446 + *
447 + * @param string $data
448 + *
449 + */
328 450 protected function attach_export_data( $data = '' ) {
329 451 $filedata = $this->get_export_file();
330 452 $filedata .= $data;
331 453
@@ -337,14 +459,24 @@
337 459
338 460 $this->empty = count( $rows ) == $columns ? true : false;
339 461 }
340 462
341 - public function uwp_get_export_users_status() {
463 + /**
464 + * Returns export user status
465 + *
466 + * @return int
467 + *
468 + */
469 + public function get_export_users_status() {
342 470 global $wpdb;
343 - $data = $wpdb->get_results("SELECT user_id FROM $this->meta_table_name WHERE 1=1");
471 + $data = $wpdb->get_results("SELECT user_id FROM $this->meta_table_name WHERE 1=1"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
344 472 $total = !empty( $data ) ? count( $data ) : 0;
345 473 $status = 100;
346 474
475 + if ( $this->per_page > $total ) {
476 + $this->per_page = $total;
477 + }
478 +
347 479 if ( $total > 0 ) {
348 480 $status = ( ( $this->per_page * $this->step ) / $total ) * 100;
349 481 }
350 482
@@ -354,9 +486,22 @@
354 486
355 487 return $status;
356 488 }
357 489
358 - public function uwp_ie_upload_file(){
490 + /**
491 + * Uploaded file handling
492 + *
493 + * @return string
494 + *
495 + */
496 + public function ie_upload_file(){
497 +
498 + if ( ! current_user_can( 'manage_options' ) ) {
499 + wp_send_json_error( array( 'message' => __( 'Permission denied.', 'userswp' ) ), 403 );
500 + }
501 +
502 + check_ajax_referer( 'uwp-ie-file-upload-nonce', 'nonce' );
503 +
359 504 $upload_data = array(
360 505 'name' => $_FILES['import_file']['name'],
361 506 'type' => $_FILES['import_file']['type'],
362 507 'tmp_name' => $_FILES['import_file']['tmp_name'],
@@ -365,12 +510,15 @@
365 510 );
366 511
367 512 header('Content-Type: text/html; charset=' . get_option('blog_charset'));
368 513
514 + add_filter( 'upload_mimes', array( $this, 'allowed_upload_mimes' ) );
369 515 $uploaded_file = wp_handle_upload( $upload_data, array('test_form' => false) );
516 + remove_filter( 'upload_mimes', array( $this, 'allowed_upload_mimes' ) );
517 +
370 518 if ( isset( $uploaded_file['url'] ) ) {
371 519 $file_loc = $uploaded_file['url'];
372 - echo $file_loc;
520 + echo esc_url( $file_loc );exit;
373 521 } else {
374 522 echo 'error';
375 523 }
376 524 exit;
@@ -375,9 +523,13 @@
375 523 }
376 524 exit;
377 525 }
378 526
379 - public function uwp_process_users_import(){
527 + /**
528 + * Processes users import
529 + *
530 + */
531 + public function process_users_import(){
380 532
381 533 $response = array();
382 534 $response['success'] = false;
383 535 $response['msg'] = __( 'Invalid import request found.', 'userswp' );
@@ -441,9 +593,9 @@
441 593 $this->imp_step = 'done';
442 594 $response['msg'] = __( 'Users import completed.', 'userswp' );
443 595 }
444 596
445 - $response['data']['msg'] = $return['msg'];
597 + $response['data']['msg'] = ! empty( $return['msg'] ) ? $return['msg'] : $response['msg'];
446 598 $response['data']['step'] = $this->imp_step;
447 599 $response['data']['done'] = $done;
448 600 } else {
449 601 $response['msg'] = __( 'No valid data found for import.', 'userswp' );
@@ -452,8 +604,14 @@
452 604 wp_send_json( $response );
453 605
454 606 }
455 607
608 + /**
609 + * Processes import step
610 + *
611 + * @return array
612 + *
613 + */
456 614 public function process_import_step() {
457 615
458 616 $errors = new WP_Error();
459 617 if(is_null($this->path)){
@@ -468,73 +626,99 @@
468 626
469 627 if ( ! empty( $rows ) ) {
470 628 foreach ( $rows as $row ) {
471 629 if( empty($row) ) {
472 - $return['msg'] = __('Row - ' .$this->imp_step. ' Error: '. 'Skipped due to invalid/no data.','userswp');
630 + $return['msg'] = sprintf(__('Row - %s Error: '. 'Skipped due to invalid/no data.','userswp'), $this->imp_step);
473 631 continue;
474 632 }
475 633
476 - $username = isset($row['uwp_account_username']) ? $row['uwp_account_username'] : '';
477 - $email = isset($row['uwp_account_email']) ? $row['uwp_account_email'] : '';
478 - $display_name = isset($row['uwp_account_display_name']) ? $row['uwp_account_display_name'] : '';
634 + $username = isset($row['username']) ? sanitize_user($row['username']) : '';
635 + $email = isset($row['email']) ? sanitize_email($row['email']) : '';
636 + $first_name = isset($row['first_name']) ? sanitize_text_field($row['first_name']) : '';
637 + $last_name = isset($row['last_name']) ? sanitize_text_field($row['last_name']) : '';
638 + $bio = isset($row['bio']) ? sanitize_textarea_field($row['bio']) : '';
639 + $display_name = isset($row['display_name']) ? sanitize_text_field($row['display_name']) : '';
479 640 $password = wp_generate_password();
480 641 $exclude = array('user_id');
481 642 $exclude = apply_filters('uwp_import_exclude_columns', $exclude, $row);
482 643
483 - if(isset($row['uwp_account_username']) && username_exists($row['uwp_account_username'])){
484 - $user = get_user_by('login', $row['uwp_account_username']);
644 + if(isset($row['username']) && username_exists($row['username'])){
645 + $user = get_user_by('login', $row['username']);
485 646 $user_id = $user->ID;
486 - $email = $row['uwp_account_email'];
487 647 if( !empty( $email ) && $update_existing = apply_filters('uwp_import_update_users', false, $row, $user_id) ) {
488 648 $args = array(
489 649 'ID' => $user_id,
490 650 'user_email' => $email,
491 - 'display_name' => $display_name
651 + 'display_name' => $display_name,
652 + 'first_name' => $first_name,
653 + 'last_name' => $last_name,
654 + 'description' => $bio,
492 655 );
493 656 wp_update_user( $args );
494 657 }
495 - } elseif(isset($row['uwp_account_email']) && email_exists($row['uwp_account_email'])){
496 - $user = get_user_by('email', $row['uwp_account_email']);
658 + } elseif(isset($row['email']) && email_exists($row['email'])){
659 + $user = get_user_by('email', $row['email']);
497 660 $user_id = $user->ID;
498 661 } elseif((int)$row['user_id'] > 0){
499 662 $user = get_user_by('ID', $row['user_id']);
500 663 if(false === $user){
501 664 $userdata = array(
502 - 'user_login' => $row['uwp_account_username'],
665 + 'user_login' => $username,
503 666 'user_email' => $email,
504 667 'user_pass' => $password,
668 + 'first_name' => $first_name,
669 + 'last_name' => $last_name,
670 + 'description' => $bio,
505 671 'display_name'=> $display_name
506 672 );
507 673 $user_id = wp_insert_user( $userdata );
674 + $notify = apply_filters('uwp_import_user_notify', 'user', $user_id);
675 + wp_new_user_notification($user_id,null, $notify); //send password reset link
508 676 } else {
509 - if( $user->user_login == $row['uwp_account_username'] ) { //check id passed in csv and existing username are same
677 + if( $user->user_login == $row['username'] ) { //check id passed in csv and existing username are same
510 678 $user_id = $row['user_id'];
511 679 if( !empty( $email ) && $email != $user->user_email && $update_existing = apply_filters('uwp_import_update_users', false, $row, $user_id)) {
512 680 $args = array(
513 681 'ID' => $user_id,
514 682 'user_email' => $email,
683 + 'first_name' => $first_name,
684 + 'last_name' => $last_name,
685 + 'description' => $bio,
515 686 'display_name' => $display_name
516 687 );
517 688 wp_update_user( $args );
518 689 }
519 690 } else {
520 - $return['msg'] = __('Row - ' .$this->imp_step. ' Error: '. 'User could not be created.','userswp');
691 + $return['msg'] = sprintf(__('Row - %s Error: '. 'User could not be created.','userswp'), $this->imp_step);
521 692 continue;
522 693 }
523 694 }
524 695 } else {
525 696 $user_id = wp_create_user( $username, $password, $email );
697 + if( !is_wp_error( $user_id ) ) {
698 + $args = array(
699 + 'ID' => $user_id,
700 + 'first_name' => $first_name,
701 + 'last_name' => $last_name,
702 + 'description' => $bio,
703 + 'display_name' => $display_name
704 + );
705 + wp_update_user( $args );
706 + }
526 707 }
527 708
528 709 if( !is_wp_error( $user_id ) ){
529 710 foreach ($row as $key => $value){
530 - if(!in_array($key, $exclude) && $update_existing = apply_filters('uwp_import_update_users', false, $row, $user_id)){
531 - $value = maybe_unserialize($value);
532 - uwp_update_usermeta($user_id, $key, $value);
711 + //Only write columns on the allowlist; denylist always wins.
712 + if ( ! $this->is_importable_column( $key ) ) {
713 + continue;
533 714 }
715 + //Never deserialize CSV input. Cast to safe scalar string.
716 + $value = $this->sanitize_import_value( $key, $value );
717 + uwp_update_usermeta($user_id, $key, $value);
534 718 }
535 719 } else {
536 - $return['msg'] = __('Row - ' .$this->imp_step. ' Error: '. $user_id->get_error_message(),'userswp');
720 + $return['msg'] = sprintf(__('Row - %s Error: %s','userswp'), $this->imp_step, $user_id->get_error_message());
537 721 continue;
538 722 }
539 723 }
540 724 $return['success'] = true;
@@ -542,8 +726,19 @@
542 726
543 727 return $return;
544 728 }
545 729
730 + /**
731 + * Returns CSV row
732 + *
733 + * @package userswp
734 + *
735 + * @param int $row
736 + * @param int $count
737 + *
738 + * @return mixed
739 + *
740 + */
546 741 public function get_csv_rows( $row = 0, $count = 1 ) {
547 742
548 743 $lc_all = setlocale( LC_ALL, 0 ); // Fix issue of fgetcsv ignores special characters when they are at the beginning of line
549 744 setlocale( LC_ALL, 'en_US.UTF-8' );
@@ -599,9 +794,8 @@
599 794 }
600 795
601 796 // A plugin may need to filter the data and meta
602 797 $userdata = apply_filters( 'uwp_import_userdata', $userdata, $usermeta );
603 - $usermeta = apply_filters( 'uwp_import_usermeta', $usermeta, $userdata );
604 798
605 799 if ( ! empty( $userdata ) ) {
606 800 $file[] = $userdata;
607 801 $f ++;
@@ -615,14 +809,26 @@
615 809 return $file;
616 810
617 811 }
618 812
813 + /**
814 + * Returns import status
815 + *
816 + * @return int
817 + *
818 + */
619 819 public function get_import_status() {
620 820 $status = 100;
621 821 return apply_filters( 'uwp_get_import_users_status', $status );
622 822 }
623 823
624 - public function uwp_get_import_users_status() {
824 + /**
825 + * Returns import user status
826 + *
827 + * @return int
828 + *
829 + */
830 + public function get_import_users_status() {
625 831
626 832 if ( $this->imp_step >= $this->total_rows ) {
627 833 $status = 100;
628 834 } else {
@@ -630,7 +836,125 @@
630 836 }
631 837
632 838 return $status;
633 839 }
840 +
841 + public function allowed_upload_mimes($mimes = array()) {
842 + $mimes['csv'] = "text/csv";
843 + return $mimes;
844 + }
845 +
846 + /**
847 + * Sanitize a single CSV import value.
848 + *
849 + * CSV data is always a plain string. There is no legitimate reason for it
850 + * to contain serialized PHP. We detect the serialization type-prefix
851 + * signatures, reject them with a log entry, and return an empty string.
852 + * All other values are cast to string and sanitized with sanitize_text_field().
853 + *
854 + * @param string $key The CSV column / meta key name.
855 + * @param mixed $value The raw value from the CSV row.
856 + * @return string A safe scalar string ready for DB insertion.
857 + */
858 + private function sanitize_import_value( $key, $value ) {
859 + // Reject serialized payloads
860 + if ( is_string( $value ) && preg_match( '/^[aAbBdDiIoOsScCnN][:;]/', ltrim( $value ) ) ) {
861 + if ( function_exists( 'uwp_log' ) ) {
862 + uwp_log( sprintf( 'Import security: serialized payload in column "%s" — discarded.', esc_attr( $key ) ) );
863 + }
864 + return '';
865 + }
866 +
867 + // File path columns: validate as a URL pointing inside the uploads directory only
868 + if ( in_array( $key, array( 'avatar_thumb', 'banner_thumb' ), true ) ) {
869 + return $this->sanitize_import_thumb( $value );
870 + }
871 +
872 + return sanitize_text_field( (string) $value );
873 + }
874 +
875 + /**
876 + * Sanitizes a thumbnail path value from CSV import.
877 + *
878 + * Validates that the given path is a real, existing file located within
879 + * the WordPress uploads directory, preventing path traversal attacks and
880 + * references to arbitrary files outside the uploads directory.
881 + *
882 + * @param string $value Raw thumbnail path value from the CSV row.
883 + * @return string Resolved absolute path if valid, empty string otherwise.
884 + */
885 + private function sanitize_import_thumb( $value ) {
886 + $value = trim( (string) $value );
887 +
888 + if ( empty( $value ) ) {
889 + return '';
890 + }
891 +
892 + // Resolve any ../ traversal attempts before comparison
893 + $real = realpath( $value );
894 +
895 + if ( $real === false ) {
896 + return ''; // Path doesn't exist on disk — reject
897 + }
898 +
899 + // Must stay within the uploads directory
900 + $uploads = wp_upload_dir();
901 + $base_dir = trailingslashit( realpath( $uploads['basedir'] ) );
902 +
903 + if ( strpos( $real . DIRECTORY_SEPARATOR, $base_dir ) !== 0 ) {
904 + if ( function_exists( 'uwp_log' ) ) {
905 + uwp_log( sprintf(
906 + 'Import security: thumb path "%s" is outside uploads directory — discarded.',
907 + esc_attr( $value )
908 + ) );
909 + }
910 + return '';
911 + }
912 +
913 + // Must be an allowed image extension
914 + $ext = strtolower( pathinfo( $real, PATHINFO_EXTENSION ) );
915 + if ( ! in_array( $ext, array( 'jpg', 'jpeg', 'png', 'gif', 'webp' ), true ) ) {
916 + return '';
917 + }
918 +
919 + return $real; // Return the resolved, canonical path
920 + }
921 +
922 + /**
923 + * Return true only when the given column name is permitted for CSV import.
924 + *
925 + * @param string $column The CSV column / meta key name.
926 + * @return bool
927 + */
928 + private function is_importable_column( $column ) {
929 + $column = strtolower( trim( (string) $column ) );
930 +
931 + // Denylist is checked first — it unconditionally blocks.
932 + if ( in_array( $column, self::$import_meta_denylist, true ) ) {
933 + return false;
934 + }
935 +
936 + return in_array( $column, self::$import_meta_allowlist, true );
937 + }
938 +
939 + /**
940 + * Escape a string to be used in a CSV export.
941 + *
942 + * @see https://hackerone.com/reports/72785
943 + *
944 + * @since 1.2.3.10
945 + *
946 + * @param string $data Data to escape.
947 + * @return string
948 + */
949 + public function escape_data( $data ) {
950 + $escape_chars = array( '=', '+', '-', '@' );
951 +
952 + if ( $data && in_array( substr( $data, 0, 1 ), $escape_chars, true ) ) {
953 + $data = " " . $data;
954 + }
955 +
956 + return $data;
957 + }
634 958
635 959 }
636 960 new UsersWP_Import_Export();