PluginProbe
UsersWP – Front-end login form, User Registration, User Profile & Members Directory plugin for WP / 1.2.76
UsersWP – Front-end login form, User Registration, User Profile & Members Directory plugin for WP v1.2.76
1.2.76 1.2.75 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 All 176 releases
← All changes | includes/class-forms.php +295 -23 1.2.67 → 1.2.76 View file →
@@ -219,8 +219,16 @@
219 219 if ( strpos( $_image_url, $content_url ) !== 0 ) {
220 220 return new WP_Error( 'invalid_image', __( 'Invalid image url.', 'userswp' ) );
221 221 }
222 222
223 + // Only allow cropping the image the current user just uploaded (normalized like $image_url).
224 + $pending_key = '_uwp_pending_' . $type . '_upload';
225 + $pending_url = get_user_meta( get_current_user_id(), $pending_key, true );
226 + $pending_url = $pending_url ? str_replace( array( 'https://', 'http://' ), '', $this->normalize_url( esc_url( $pending_url ) ) ) : '';
227 + if ( empty( $pending_url ) || $pending_url !== $_image_url ) {
228 + return new WP_Error( 'crop_session_expired', __( 'Your image upload could not be verified. Please upload the image again.', 'userswp' ) );
229 + }
230 +
223 231 $filetype = wp_check_filetype( $image_url );
224 232
225 233 if ( empty( $filetype['ext'] ) ) {
226 234 return new WP_Error( 'invalid_image', __( 'Invalid image type.', 'userswp' ) );
@@ -281,8 +289,15 @@
281 289 wp_die( esc_html__( 'Something went wrong. Please contact site admin.', 'userswp' ), 403 );
282 290 }
283 291
284 292 $cropped = uwp_resizeThumbnailImage( $thumb_image_location, $image_path, $x, $y, $w, $h, $scale );
293 +
294 + // Resize returns a path even on failure; bail before touching meta or files so the crop can be retried.
295 + clearstatcache( true, $thumb_image_location );
296 + if ( ! is_file( $thumb_image_location ) ) {
297 + return new WP_Error( 'crop_failed', __( 'Could not crop the image. Please try again.', 'userswp' ) );
298 + }
299 +
285 300 $cropped = str_replace( $upload_path, $upload_url, $cropped );
286 301
287 302 // Remove previous avatar/banner
288 303 $unlink_img = '';
@@ -297,13 +312,34 @@
297 312 } else {
298 313 uwp_update_usermeta( $user_id, 'banner_thumb', $cropped );
299 314 }
300 315
301 - if ( $unlink_img && $unlink_img != $thumb_image_location && is_file( $unlink_img ) && file_exists( $unlink_img ) ) {
302 - @unlink( $unlink_img );
303 - $unlink_ori_img = str_replace( '_uwp_' . $type . '_thumb' . '.', '.', $unlink_img );
304 - if ( is_file( $unlink_ori_img ) && file_exists( $unlink_ori_img ) ) {
305 - @unlink( $unlink_ori_img );
316 + $original_key = '_uwp_' . $type . '_original';
317 + $prev_original = get_user_meta( $user_id, $original_key, true );
318 +
319 + delete_user_meta( get_current_user_id(), $pending_key );
320 + $relative_original = ltrim( wp_normalize_path( str_replace( wp_normalize_path( untrailingslashit( $upload_path ) ), '', wp_normalize_path( $image_path ) ) ), '/' );
321 + update_user_meta( $user_id, $original_key, $relative_original );
322 +
323 + // Enforce containment inside uploads before deleting, matching upload_file_remove().
324 + $real_upload_path = realpath( $upload_path );
325 + $real_unlink_img = $unlink_img ? realpath( $unlink_img ) : false;
326 +
327 + if ( $real_upload_path && $real_unlink_img && realpath( $thumb_image_location ) !== $real_unlink_img
328 + && false !== strpos( basename( $real_unlink_img ), $thumb_postfix . '.' )
329 + && 0 === strpos( $real_unlink_img, $real_upload_path . DIRECTORY_SEPARATOR )
330 + && is_file( $real_unlink_img ) ) {
331 + wp_delete_file( $real_unlink_img );
332 +
333 + // Delete the previous source only if it is the exact file this user cropped.
334 + $unlink_ori_img = str_replace( $thumb_postfix . '.', '.', $real_unlink_img );
335 + $real_unlink_ori_img = realpath( $unlink_ori_img );
336 + $expected_original = $prev_original ? realpath( untrailingslashit( $upload_path ) . '/' . $prev_original ) : false;
337 + if ( $expected_original && $real_unlink_ori_img && $expected_original === $real_unlink_ori_img
338 + && realpath( $image_path ) !== $real_unlink_ori_img
339 + && 0 === strpos( $real_unlink_ori_img, $real_upload_path . DIRECTORY_SEPARATOR )
340 + && is_file( $real_unlink_ori_img ) ) {
341 + wp_delete_file( $real_unlink_ori_img );
306 342 }
307 343 }
308 344 }
309 345
@@ -390,8 +426,13 @@
390 426 } else {
391 427 // Do nothing
392 428 }
393 429
430 + if ( in_array( $type, array( 'avatar', 'banner' ), true ) ) {
431 + delete_user_meta( $user_id, '_uwp_' . $type . '_original' );
432 + delete_user_meta( get_current_user_id(), '_uwp_pending_' . $type . '_upload' );
433 + }
434 +
394 435 if ( is_admin() ) {
395 436 if ( $user_id == get_current_user_id() ) {
396 437 $redirect_url = admin_url( 'profile.php' );
397 438 } else {
@@ -1216,8 +1257,20 @@
1216 1257 }
1217 1258 }
1218 1259 }
1219 1260
1261 + if ( wp_doing_ajax() && is_wp_error( $user ) && $this->wordfence_2fa_available() ) {
1262 + $wfls_2fa = $this->check_wordfence_2fa( $user, $result );
1263 + if ( ! empty( $wfls_2fa ) ) {
1264 + wp_send_json_success(
1265 + array(
1266 + 'html' => $wfls_2fa,
1267 + 'is_2fa' => true,
1268 + )
1269 + );
1270 + }
1271 + }
1272 +
1220 1273 if ( is_wp_error( $user ) ) {
1221 1274 $message = aui()->alert(
1222 1275 array(
1223 1276 'type' => 'error',
@@ -1436,8 +1489,110 @@
1436 1489
1437 1490 return ob_get_clean();
1438 1491 }
1439 1492
1493 + /**
1494 + * Checks if the Wordfence Login Security module (2FA) is available.
1495 + *
1496 + * @since 1.2.5
1497 + * @package userswp
1498 + *
1499 + * @return bool
1500 + */
1501 + public function wordfence_2fa_available() {
1502 + return class_exists( '\WordfenceLS\Controller_Users' ) && class_exists( '\WordfenceLS\Controller_TOTP' );
1503 + }
1504 +
1505 + /**
1506 + * Checks whether Wordfence's 2FA requires a verification code for the
1507 + * failed login attempt and, if so, returns the markup for the code entry form.
1508 + *
1509 + * @since 1.2.5
1510 + * @package userswp
1511 + *
1512 + * @param WP_Error $error The error returned by wp_signon().
1513 + * @param array $result The validated login fields (username/password).
1514 + *
1515 + * @return string|void The 2FA form markup, or nothing if not applicable.
1516 + */
1517 + public function check_wordfence_2fa( $error, $result ) {
1518 + if ( 1 == uwp_get_option( 'disable_wordfence_2fa' ) ) {
1519 + return;
1520 + }
1521 +
1522 + if ( ! $this->wordfence_2fa_available() ) {
1523 + return;
1524 + }
1525 +
1526 + if ( ! is_wp_error( $error ) || 'wfls_twofactor_required' !== $error->get_error_code() ) {
1527 + return;
1528 + }
1529 +
1530 + $username = ! empty( $result['username'] ) ? $result['username'] : '';
1531 + if ( empty( $username ) ) {
1532 + return;
1533 + }
1534 +
1535 + $user = is_email( $username ) ? get_user_by( 'email', $username ) : get_user_by( 'login', $username );
1536 + if ( ! $user ) {
1537 + return;
1538 + }
1539 +
1540 + if ( ! \WordfenceLS\Controller_Users::shared()->has_2fa_active( $user ) ) {
1541 + return;
1542 + }
1543 +
1544 + if ( \WordfenceLS\Controller_Users::shared()->has_remembered_2fa( $user ) ) {
1545 + return;
1546 + }
1547 +
1548 + $login_nonce = wp_create_nonce( 'uwp-wfls-2fa-' . $user->ID );
1549 +
1550 + ob_start();
1551 + ?>
1552 +
1553 + <div class="uwp-2fa-methods-wrap">
1554 + <form name="validate_2fa_form" id="validate_2fa_form" class="validate_2fa_form" action="" method="post"
1555 + autocomplete="off">
1556 + <input type="hidden" name="provider" id="provider" value="wordfence"/>
1557 + <input type="hidden" name="uwp-auth-id" id="uwp-auth-id" value="<?php echo esc_attr( $user->ID ); ?>"/>
1558 + <input type="hidden" name="wp-auth-nonce" id="wp-auth-nonce"
1559 + value="<?php echo esc_attr( $login_nonce ); ?>"/>
1560 +
1561 + <p><?php esc_html_e( 'Please enter the authentication code from your two-factor authentication app, or a recovery code, to login:', 'userswp' ); ?></p>
1562 +
1563 + <?php
1564 + echo aui()->input(
1565 + array( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
1566 + 'type' => 'text',
1567 + 'id' => 'authcode',
1568 + 'name' => 'authcode',
1569 + 'placeholder' => esc_attr__( 'Authentication Code', 'userswp' ),
1570 + 'value' => '',
1571 + 'label' => esc_html__( 'Authentication Code', 'userswp' ),
1572 + 'extra_attributes' => array(
1573 + 'autocomplete' => 'one-time-code',
1574 + ),
1575 + )
1576 + );
1577 +
1578 + echo aui()->button(
1579 + array( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
1580 + 'type' => 'submit',
1581 + 'class' => 'btn btn-primary btn-block text-uppercase uwp-2fa-submit',
1582 + 'name' => 'submit',
1583 + 'icon' => '',
1584 + 'content' => esc_html__( 'Log In', 'userswp' ),
1585 + )
1586 + );
1587 + ?>
1588 + </form>
1589 + </div>
1590 +
1591 + <?php
1592 + return ob_get_clean();
1593 + }
1594 +
1440 1595 public function get_wp2fa_provider_for_user( $user ) {
1441 1596 if ( class_exists( '\WP2FA\Authenticator\Login' ) && method_exists( '\WP2FA\Authenticator\Login', 'get_available_providers_for_user' ) ) {
1442 1597 $provider = \WP2FA\Authenticator\Login::get_available_providers_for_user( $user );
1443 1598 if ( is_array( $provider ) ) {
@@ -1501,8 +1656,77 @@
1501 1656
1502 1657 return false;
1503 1658 }
1504 1659
1660 + /**
1661 + * Validates the Wordfence 2FA code submitted from the uwp-2fa form and,
1662 + * if valid, completes the login by setting the auth cookie.
1663 + *
1664 + * @since 1.2.5
1665 + * @package userswp
1666 + *
1667 + * @param WP_User $user The user attempting to complete 2FA login.
1668 + *
1669 + * @return void
1670 + */
1671 + public function process_login_wordfence_2fa( $user ) {
1672 + if ( ! $this->wordfence_2fa_available() ) {
1673 + $message = aui()->alert(
1674 + array(
1675 + 'type' => 'error',
1676 + 'content' => __( 'Invalid request! Please try again.', 'userswp' ),
1677 + )
1678 + );
1679 +
1680 + wp_send_json_error( array( 'message' => $message ) );
1681 + }
1682 +
1683 + $nonce = ( isset( $_POST['wp-auth-nonce'] ) ) ? sanitize_textarea_field( wp_unslash( $_POST['wp-auth-nonce'] ) ) : '';
1684 +
1685 + if ( ! wp_verify_nonce( $nonce, 'uwp-wfls-2fa-' . $user->ID ) ) {
1686 + $message = aui()->alert(
1687 + array(
1688 + 'type' => 'error',
1689 + 'content' => __( 'Invalid request! Please try again.', 'userswp' ),
1690 + )
1691 + );
1692 +
1693 + wp_send_json_error( array( 'message' => $message ) );
1694 + }
1695 +
1696 + $code = isset( $_POST['authcode'] ) ? sanitize_text_field( wp_unslash( $_POST['authcode'] ) ) : '';
1697 +
1698 + if ( empty( $code ) || true !== \WordfenceLS\Controller_TOTP::shared()->validate_2fa( $user, $code ) ) {
1699 + do_action( 'wp_login_failed', $user->user_login );
1700 +
1701 + $message = aui()->alert(
1702 + array(
1703 + 'type' => 'error',
1704 + 'content' => __( 'Invalid verification code.', 'userswp' ),
1705 + )
1706 + );
1707 +
1708 + wp_send_json_error( array( 'message' => $message ) );
1709 + }
1710 +
1711 + $remember = ( isset( $_REQUEST['rememberme'] ) ) ? filter_var( $_REQUEST['rememberme'], FILTER_VALIDATE_BOOLEAN ) : false;
1712 +
1713 + // Complete the login the same way wp_signon() would have, now that 2FA has been verified.
1714 + wp_set_auth_cookie( $user->ID, $remember );
1715 + wp_set_current_user( $user->ID );
1716 +
1717 + do_action( 'wp_login', $user->user_login, $user );
1718 +
1719 + $message = aui()->alert(
1720 + array(
1721 + 'type' => 'success',
1722 + 'content' => __( 'Validation successful. Redirecting...', 'userswp' ),
1723 + )
1724 + );
1725 +
1726 + wp_send_json_success( array( 'message' => $message ) );
1727 + }
1728 +
1505 1729 public function process_login_2fa() {
1506 1730 global $wp2fa;
1507 1731
1508 1732 if ( ! isset( $_POST['uwp-auth-id'], $_POST['wp-auth-nonce'] ) ) {
@@ -1522,8 +1746,20 @@
1522 1746
1523 1747 wp_send_json_error( array( 'message' => $message ) );
1524 1748 }
1525 1749
1750 + if ( isset( $_POST['provider'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
1751 + $provider = sanitize_textarea_field( wp_unslash( $_POST['provider'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
1752 + } else {
1753 + $provider = '';
1754 + }
1755 +
1756 + if ( 'wordfence' === $provider ) {
1757 + $this->process_login_wordfence_2fa( $user );
1758 +
1759 + return;
1760 + }
1761 +
1526 1762 $nonce = ( isset( $_POST['wp-auth-nonce'] ) ) ? sanitize_textarea_field( wp_unslash( $_POST['wp-auth-nonce'] ) ) : '';
1527 1763
1528 1764 if ( true !== \WP2FA\Authenticator\Login::verify_login_nonce( $user->ID, $nonce ) ) {
1529 1765 $message = aui()->alert(
@@ -1535,14 +1771,8 @@
1535 1771
1536 1772 wp_send_json_error( array( 'message' => $message ) );
1537 1773 }
1538 1774
1539 - if ( isset( $_POST['provider'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
1540 - $provider = sanitize_textarea_field( wp_unslash( $_POST['provider'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
1541 - } else {
1542 - $provider = '';
1543 - }
1544 -
1545 1775 $error = '';
1546 1776
1547 1777 try {
1548 1778 $is_enabled = \WP2FA\Admin\Controllers\Settings::is_provider_enabled_for_role( \WP2FA\Admin\Helpers\User_Helper::get_user_role( $user ), $provider );
@@ -1744,9 +1974,12 @@
1744 1974 }
1745 1975
1746 1976 do_action( 'uwp_after_validate', $result, 'forgot', $data );
1747 1977
1748 - $user_data = get_user_by( 'email', $data['email'] );
1978 + $login_or_email = trim( $data['email'] );
1979 + $user_data = is_email( $login_or_email )
1980 + ? get_user_by( 'email', $login_or_email )
1981 + : get_user_by( 'login', $login_or_email );
1749 1982
1750 1983 // if no user we fake it and bail
1751 1984 if ( ! $user_data ) {
1752 1985 $args = apply_filters(
@@ -1752,9 +1985,9 @@
1752 1985 $args = apply_filters(
1753 1986 'uwp_forgot_error_message',
1754 1987 array(
1755 1988 'type' => 'error',
1756 - 'content' => __( 'Invalid email or user doesn\'t exists.', 'userswp' ),
1989 + 'content' => __( 'Invalid username/email or user doesn\'t exist.', 'userswp' ),
1757 1990 )
1758 1991 );
1759 1992
1760 1993 $message = aui()->alert( $args );
@@ -2124,8 +2357,21 @@
2124 2357 unset( $uploads_result[ $upload_file_key ] );
2125 2358 }
2126 2359 }
2127 2360
2361 + global $wpdb;
2362 + $file_field_names = $wpdb->get_col(
2363 + $wpdb->prepare(
2364 + "SELECT htmlvar_name FROM " . uwp_get_table_prefix() . "uwp_form_fields WHERE form_type = %s AND field_type IN ('file','image')",
2365 + 'account'
2366 + )
2367 + );
2368 + foreach ( $file_field_names as $file_field_name ) {
2369 + if ( isset( $result[ $file_field_name ] ) && ! isset( $uploads_result[ $file_field_name ] ) ) {
2370 + unset( $result[ $file_field_name ] );
2371 + }
2372 + }
2373 +
2128 2374 $result = array_merge( $result, $uploads_result );
2129 2375
2130 2376 $args = array(
2131 2377 'ID' => get_current_user_id(),
@@ -2424,22 +2670,39 @@
2424 2670 }
2425 2671
2426 2672 $unlink_file = untrailingslashit( $upload_path ) . '/' . trim( $value, '/\\' );
2427 2673
2428 - if ( is_file( $unlink_file ) && file_exists( $unlink_file ) ) {
2429 - wp_delete_file( $unlink_file );
2674 + // Canonicalize and enforce containment inside the uploads directory before deleting.
2675 + $real_upload_path = realpath( $upload_path );
2676 + $real_unlink_file = realpath( $unlink_file );
2430 2677
2431 - // For avatar/banner, also remove the original (non-thumb) file.
2678 + if ( $real_upload_path && $real_unlink_file && is_file( $real_unlink_file )
2679 + && strpos( $real_unlink_file, $real_upload_path . DIRECTORY_SEPARATOR ) === 0 ) {
2680 + wp_delete_file( $real_unlink_file );
2681 +
2682 + // For avatar/banner, also remove the original (non-thumb) file, only if it is the exact file this user cropped.
2432 2683 if ( $type ) {
2433 - $unlink_ori_file = str_replace( '_uwp_' . $type . '_thumb' . '.', '.', $unlink_file );
2684 + $unlink_ori_file = str_replace( '_uwp_' . $type . '_thumb' . '.', '.', $real_unlink_file );
2685 + $real_unlink_ori_file = realpath( $unlink_ori_file );
2686 + $prev_original = get_user_meta( $user_id, '_uwp_' . $type . '_original', true );
2687 + $expected_original = $prev_original ? realpath( untrailingslashit( $upload_path ) . '/' . $prev_original ) : false;
2434 2688
2435 - if ( is_file( $unlink_ori_file ) && file_exists( $unlink_ori_file ) ) {
2436 - wp_delete_file( $unlink_ori_file );
2689 + if ( $expected_original && $real_unlink_ori_file && $expected_original === $real_unlink_ori_file
2690 + && $real_unlink_ori_file !== $real_unlink_file
2691 + && is_file( $real_unlink_ori_file )
2692 + && strpos( $real_unlink_ori_file, $real_upload_path . DIRECTORY_SEPARATOR ) === 0 ) {
2693 + wp_delete_file( $real_unlink_ori_file );
2437 2694 }
2438 2695 }
2439 2696 }
2440 2697 }
2441 2698
2699 + // Clear crop bookkeeping meta (pending upload is stored against the uploader).
2700 + if ( $type ) {
2701 + delete_user_meta( $user_id, '_uwp_' . $type . '_original' );
2702 + delete_user_meta( get_current_user_id(), '_uwp_pending_' . $type . '_upload' );
2703 + }
2704 +
2442 2705 wp_send_json_success();
2443 2706
2444 2707 wp_die();
2445 2708 }
@@ -3917,17 +4180,26 @@
3917 4180 $site_title = uwp_get_form_label( $field );
3918 4181 $required_msg = ( ! empty( $field->is_required ) && $field->required_msg != '') ? __( stripslashes( $field->required_msg ), 'userswp' ) : '';
3919 4182 $validation_text = ! empty( $field->validation_msg ) ? __( stripslashes( $field->validation_msg ), 'userswp' ) : '';
3920 4183
4184 + $is_forgot_email = ( $form_type === 'forgot' && $field->htmlvar_name === 'email' );
4185 + $input_type = $is_forgot_email ? 'text' : 'email';
4186 + if ( $is_forgot_email ) {
4187 + $site_title = __( 'Username or Email', 'userswp' );
4188 + $placeholder = $site_title . ( ! empty( $field->is_required ) ? ' *' : '' );
4189 + } else {
4190 + $placeholder = uwp_get_field_placeholder( $field );
4191 + }
4192 +
3921 4193 if ( $design_style ) {
3922 4194 $required = ! empty( $field->is_required ) ? ' <span class="text-danger">*</span>' : '';
3923 4195
3924 4196 echo aui()->input(
3925 4197 array( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
3926 - 'type' => 'email',
4198 + 'type' => $input_type,
3927 4199 'id' => esc_attr( $field->htmlvar_name ),
3928 4200 'name' => esc_attr( $field->htmlvar_name ),
3929 - 'placeholder' => esc_attr( uwp_get_field_placeholder( $field ) ),
4201 + 'placeholder' => esc_attr( $placeholder ),
3930 4202 'title' => esc_html( $site_title ),
3931 4203 'value' => esc_attr( wp_unslash( $value ) ),
3932 4204 'required' => (bool) $field->is_required,
3933 4205 'validation_text' => $validation_text != '' ? esc_attr( $validation_text ) : esc_attr( $required_msg ),
@@ -3963,9 +4235,9 @@
3963 4235
3964 4236 <input name="<?php echo esc_attr( $field->htmlvar_name ); ?>"
3965 4237 class="<?php echo esc_attr( $field->css_class ); ?> uwp_textfield <?php echo esc_attr( $bs_form_control ); ?>"
3966 4238 id="<?php echo esc_attr( $field->htmlvar_name ); ?>"
3967 - placeholder="<?php echo esc_attr( uwp_get_field_placeholder( $field ) ); ?>"
4239 + placeholder="<?php echo esc_attr( $placeholder ); ?>"
3968 4240 value="<?php echo esc_attr( stripslashes( $value ) ); ?>"
3969 4241 title="<?php echo esc_attr( $site_title ); ?>"
3970 4242 <?php
3971 4243 if ( $field->is_required == 1 ) {
@@ -3971,9 +4243,9 @@
3971 4243 if ( $field->is_required == 1 ) {
3972 4244 echo 'required="required"';
3973 4245 }
3974 4246 ?>
3975 - type="email"
4247 + type="<?php echo esc_attr( $input_type ); ?>"
3976 4248 />
3977 4249 <span class="uwp_message_note"><?php echo wp_kses_post( uwp_get_field_description( $field ) ); ?></span>
3978 4250 <?php if ( $field->is_required ) { ?>
3979 4251 <span class="uwp_message_error invalid-feedback"><?php echo esc_html__( stripslashes( $field->required_msg ), 'userswp' ); ?></span>