| @@ -1216,8 +1216,20 @@ | ||
| 1216 | 1216 | } |
| 1217 | 1217 | } |
| 1218 | 1218 | } |
| 1219 | 1219 | |
| 1220 | + if ( wp_doing_ajax() && is_wp_error( $user ) && $this->wordfence_2fa_available() ) { | |
| 1221 | + $wfls_2fa = $this->check_wordfence_2fa( $user, $result ); | |
| 1222 | + if ( ! empty( $wfls_2fa ) ) { | |
| 1223 | + wp_send_json_success( | |
| 1224 | + array( | |
| 1225 | + 'html' => $wfls_2fa, | |
| 1226 | + 'is_2fa' => true, | |
| 1227 | + ) | |
| 1228 | + ); | |
| 1229 | + } | |
| 1230 | + } | |
| 1231 | + | |
| 1220 | 1232 | if ( is_wp_error( $user ) ) { |
| 1221 | 1233 | $message = aui()->alert( |
| 1222 | 1234 | array( |
| 1223 | 1235 | 'type' => 'error', |
| @@ -1436,8 +1448,110 @@ | ||
| 1436 | 1448 | |
| 1437 | 1449 | return ob_get_clean(); |
| 1438 | 1450 | } |
| 1439 | 1451 | |
| 1452 | + /** | |
| 1453 | + * Checks if the Wordfence Login Security module (2FA) is available. | |
| 1454 | + * | |
| 1455 | + * @since 1.2.5 | |
| 1456 | + * @package userswp | |
| 1457 | + * | |
| 1458 | + * @return bool | |
| 1459 | + */ | |
| 1460 | + public function wordfence_2fa_available() { | |
| 1461 | + return class_exists( '\WordfenceLS\Controller_Users' ) && class_exists( '\WordfenceLS\Controller_TOTP' ); | |
| 1462 | + } | |
| 1463 | + | |
| 1464 | + /** | |
| 1465 | + * Checks whether Wordfence's 2FA requires a verification code for the | |
| 1466 | + * failed login attempt and, if so, returns the markup for the code entry form. | |
| 1467 | + * | |
| 1468 | + * @since 1.2.5 | |
| 1469 | + * @package userswp | |
| 1470 | + * | |
| 1471 | + * @param WP_Error $error The error returned by wp_signon(). | |
| 1472 | + * @param array $result The validated login fields (username/password). | |
| 1473 | + * | |
| 1474 | + * @return string|void The 2FA form markup, or nothing if not applicable. | |
| 1475 | + */ | |
| 1476 | + public function check_wordfence_2fa( $error, $result ) { | |
| 1477 | + if ( 1 == uwp_get_option( 'disable_wordfence_2fa' ) ) { | |
| 1478 | + return; | |
| 1479 | + } | |
| 1480 | + | |
| 1481 | + if ( ! $this->wordfence_2fa_available() ) { | |
| 1482 | + return; | |
| 1483 | + } | |
| 1484 | + | |
| 1485 | + if ( ! is_wp_error( $error ) || 'wfls_twofactor_required' !== $error->get_error_code() ) { | |
| 1486 | + return; | |
| 1487 | + } | |
| 1488 | + | |
| 1489 | + $username = ! empty( $result['username'] ) ? $result['username'] : ''; | |
| 1490 | + if ( empty( $username ) ) { | |
| 1491 | + return; | |
| 1492 | + } | |
| 1493 | + | |
| 1494 | + $user = is_email( $username ) ? get_user_by( 'email', $username ) : get_user_by( 'login', $username ); | |
| 1495 | + if ( ! $user ) { | |
| 1496 | + return; | |
| 1497 | + } | |
| 1498 | + | |
| 1499 | + if ( ! \WordfenceLS\Controller_Users::shared()->has_2fa_active( $user ) ) { | |
| 1500 | + return; | |
| 1501 | + } | |
| 1502 | + | |
| 1503 | + if ( \WordfenceLS\Controller_Users::shared()->has_remembered_2fa( $user ) ) { | |
| 1504 | + return; | |
| 1505 | + } | |
| 1506 | + | |
| 1507 | + $login_nonce = wp_create_nonce( 'uwp-wfls-2fa-' . $user->ID ); | |
| 1508 | + | |
| 1509 | + ob_start(); | |
| 1510 | + ?> | |
| 1511 | + | |
| 1512 | + <div class="uwp-2fa-methods-wrap"> | |
| 1513 | + <form name="validate_2fa_form" id="validate_2fa_form" class="validate_2fa_form" action="" method="post" | |
| 1514 | + autocomplete="off"> | |
| 1515 | + <input type="hidden" name="provider" id="provider" value="wordfence"/> | |
| 1516 | + <input type="hidden" name="uwp-auth-id" id="uwp-auth-id" value="<?php echo esc_attr( $user->ID ); ?>"/> | |
| 1517 | + <input type="hidden" name="wp-auth-nonce" id="wp-auth-nonce" | |
| 1518 | + value="<?php echo esc_attr( $login_nonce ); ?>"/> | |
| 1519 | + | |
| 1520 | + <p><?php esc_html_e( 'Please enter the authentication code from your two-factor authentication app, or a recovery code, to login:', 'userswp' ); ?></p> | |
| 1521 | + | |
| 1522 | + <?php | |
| 1523 | + echo aui()->input( | |
| 1524 | + array( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped | |
| 1525 | + 'type' => 'text', | |
| 1526 | + 'id' => 'authcode', | |
| 1527 | + 'name' => 'authcode', | |
| 1528 | + 'placeholder' => esc_attr__( 'Authentication Code', 'userswp' ), | |
| 1529 | + 'value' => '', | |
| 1530 | + 'label' => esc_html__( 'Authentication Code', 'userswp' ), | |
| 1531 | + 'extra_attributes' => array( | |
| 1532 | + 'autocomplete' => 'one-time-code', | |
| 1533 | + ), | |
| 1534 | + ) | |
| 1535 | + ); | |
| 1536 | + | |
| 1537 | + echo aui()->button( | |
| 1538 | + array( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped | |
| 1539 | + 'type' => 'submit', | |
| 1540 | + 'class' => 'btn btn-primary btn-block text-uppercase uwp-2fa-submit', | |
| 1541 | + 'name' => 'submit', | |
| 1542 | + 'icon' => '', | |
| 1543 | + 'content' => esc_html__( 'Log In', 'userswp' ), | |
| 1544 | + ) | |
| 1545 | + ); | |
| 1546 | + ?> | |
| 1547 | + </form> | |
| 1548 | + </div> | |
| 1549 | + | |
| 1550 | + <?php | |
| 1551 | + return ob_get_clean(); | |
| 1552 | + } | |
| 1553 | + | |
| 1440 | 1554 | public function get_wp2fa_provider_for_user( $user ) { |
| 1441 | 1555 | if ( class_exists( '\WP2FA\Authenticator\Login' ) && method_exists( '\WP2FA\Authenticator\Login', 'get_available_providers_for_user' ) ) { |
| 1442 | 1556 | $provider = \WP2FA\Authenticator\Login::get_available_providers_for_user( $user ); |
| 1443 | 1557 | if ( is_array( $provider ) ) { |
| @@ -1501,8 +1615,77 @@ | ||
| 1501 | 1615 | |
| 1502 | 1616 | return false; |
| 1503 | 1617 | } |
| 1504 | 1618 | |
| 1619 | + /** | |
| 1620 | + * Validates the Wordfence 2FA code submitted from the uwp-2fa form and, | |
| 1621 | + * if valid, completes the login by setting the auth cookie. | |
| 1622 | + * | |
| 1623 | + * @since 1.2.5 | |
| 1624 | + * @package userswp | |
| 1625 | + * | |
| 1626 | + * @param WP_User $user The user attempting to complete 2FA login. | |
| 1627 | + * | |
| 1628 | + * @return void | |
| 1629 | + */ | |
| 1630 | + public function process_login_wordfence_2fa( $user ) { | |
| 1631 | + if ( ! $this->wordfence_2fa_available() ) { | |
| 1632 | + $message = aui()->alert( | |
| 1633 | + array( | |
| 1634 | + 'type' => 'error', | |
| 1635 | + 'content' => __( 'Invalid request! Please try again.', 'userswp' ), | |
| 1636 | + ) | |
| 1637 | + ); | |
| 1638 | + | |
| 1639 | + wp_send_json_error( array( 'message' => $message ) ); | |
| 1640 | + } | |
| 1641 | + | |
| 1642 | + $nonce = ( isset( $_POST['wp-auth-nonce'] ) ) ? sanitize_textarea_field( wp_unslash( $_POST['wp-auth-nonce'] ) ) : ''; | |
| 1643 | + | |
| 1644 | + if ( ! wp_verify_nonce( $nonce, 'uwp-wfls-2fa-' . $user->ID ) ) { | |
| 1645 | + $message = aui()->alert( | |
| 1646 | + array( | |
| 1647 | + 'type' => 'error', | |
| 1648 | + 'content' => __( 'Invalid request! Please try again.', 'userswp' ), | |
| 1649 | + ) | |
| 1650 | + ); | |
| 1651 | + | |
| 1652 | + wp_send_json_error( array( 'message' => $message ) ); | |
| 1653 | + } | |
| 1654 | + | |
| 1655 | + $code = isset( $_POST['authcode'] ) ? sanitize_text_field( wp_unslash( $_POST['authcode'] ) ) : ''; | |
| 1656 | + | |
| 1657 | + if ( empty( $code ) || true !== \WordfenceLS\Controller_TOTP::shared()->validate_2fa( $user, $code ) ) { | |
| 1658 | + do_action( 'wp_login_failed', $user->user_login ); | |
| 1659 | + | |
| 1660 | + $message = aui()->alert( | |
| 1661 | + array( | |
| 1662 | + 'type' => 'error', | |
| 1663 | + 'content' => __( 'Invalid verification code.', 'userswp' ), | |
| 1664 | + ) | |
| 1665 | + ); | |
| 1666 | + | |
| 1667 | + wp_send_json_error( array( 'message' => $message ) ); | |
| 1668 | + } | |
| 1669 | + | |
| 1670 | + $remember = ( isset( $_REQUEST['rememberme'] ) ) ? filter_var( $_REQUEST['rememberme'], FILTER_VALIDATE_BOOLEAN ) : false; | |
| 1671 | + | |
| 1672 | + // Complete the login the same way wp_signon() would have, now that 2FA has been verified. | |
| 1673 | + wp_set_auth_cookie( $user->ID, $remember ); | |
| 1674 | + wp_set_current_user( $user->ID ); | |
| 1675 | + | |
| 1676 | + do_action( 'wp_login', $user->user_login, $user ); | |
| 1677 | + | |
| 1678 | + $message = aui()->alert( | |
| 1679 | + array( | |
| 1680 | + 'type' => 'success', | |
| 1681 | + 'content' => __( 'Validation successful. Redirecting...', 'userswp' ), | |
| 1682 | + ) | |
| 1683 | + ); | |
| 1684 | + | |
| 1685 | + wp_send_json_success( array( 'message' => $message ) ); | |
| 1686 | + } | |
| 1687 | + | |
| 1505 | 1688 | public function process_login_2fa() { |
| 1506 | 1689 | global $wp2fa; |
| 1507 | 1690 | |
| 1508 | 1691 | if ( ! isset( $_POST['uwp-auth-id'], $_POST['wp-auth-nonce'] ) ) { |
| @@ -1522,8 +1705,20 @@ | ||
| 1522 | 1705 | |
| 1523 | 1706 | wp_send_json_error( array( 'message' => $message ) ); |
| 1524 | 1707 | } |
| 1525 | 1708 | |
| 1709 | + if ( isset( $_POST['provider'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing | |
| 1710 | + $provider = sanitize_textarea_field( wp_unslash( $_POST['provider'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing | |
| 1711 | + } else { | |
| 1712 | + $provider = ''; | |
| 1713 | + } | |
| 1714 | + | |
| 1715 | + if ( 'wordfence' === $provider ) { | |
| 1716 | + $this->process_login_wordfence_2fa( $user ); | |
| 1717 | + | |
| 1718 | + return; | |
| 1719 | + } | |
| 1720 | + | |
| 1526 | 1721 | $nonce = ( isset( $_POST['wp-auth-nonce'] ) ) ? sanitize_textarea_field( wp_unslash( $_POST['wp-auth-nonce'] ) ) : ''; |
| 1527 | 1722 | |
| 1528 | 1723 | if ( true !== \WP2FA\Authenticator\Login::verify_login_nonce( $user->ID, $nonce ) ) { |
| 1529 | 1724 | $message = aui()->alert( |
| @@ -1535,14 +1730,8 @@ | ||
| 1535 | 1730 | |
| 1536 | 1731 | wp_send_json_error( array( 'message' => $message ) ); |
| 1537 | 1732 | } |
| 1538 | 1733 | |
| 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 | 1734 | $error = ''; |
| 1546 | 1735 | |
| 1547 | 1736 | try { |
| 1548 | 1737 | $is_enabled = \WP2FA\Admin\Controllers\Settings::is_provider_enabled_for_role( \WP2FA\Admin\Helpers\User_Helper::get_user_role( $user ), $provider ); |
| @@ -1744,9 +1933,12 @@ | ||
| 1744 | 1933 | } |
| 1745 | 1934 | |
| 1746 | 1935 | do_action( 'uwp_after_validate', $result, 'forgot', $data ); |
| 1747 | 1936 | |
| 1748 | - $user_data = get_user_by( 'email', $data['email'] ); | |
| 1937 | + $login_or_email = trim( $data['email'] ); | |
| 1938 | + $user_data = is_email( $login_or_email ) | |
| 1939 | + ? get_user_by( 'email', $login_or_email ) | |
| 1940 | + : get_user_by( 'login', $login_or_email ); | |
| 1749 | 1941 | |
| 1750 | 1942 | // if no user we fake it and bail |
| 1751 | 1943 | if ( ! $user_data ) { |
| 1752 | 1944 | $args = apply_filters( |
| @@ -1752,9 +1944,9 @@ | ||
| 1752 | 1944 | $args = apply_filters( |
| 1753 | 1945 | 'uwp_forgot_error_message', |
| 1754 | 1946 | array( |
| 1755 | 1947 | 'type' => 'error', |
| 1756 | - 'content' => __( 'Invalid email or user doesn\'t exists.', 'userswp' ), | |
| 1948 | + 'content' => __( 'Invalid username/email or user doesn\'t exist.', 'userswp' ), | |
| 1757 | 1949 | ) |
| 1758 | 1950 | ); |
| 1759 | 1951 | |
| 1760 | 1952 | $message = aui()->alert( $args ); |
| @@ -2124,8 +2316,21 @@ | ||
| 2124 | 2316 | unset( $uploads_result[ $upload_file_key ] ); |
| 2125 | 2317 | } |
| 2126 | 2318 | } |
| 2127 | 2319 | |
| 2320 | + global $wpdb; | |
| 2321 | + $file_field_names = $wpdb->get_col( | |
| 2322 | + $wpdb->prepare( | |
| 2323 | + "SELECT htmlvar_name FROM " . uwp_get_table_prefix() . "uwp_form_fields WHERE form_type = %s AND field_type IN ('file','image')", | |
| 2324 | + 'account' | |
| 2325 | + ) | |
| 2326 | + ); | |
| 2327 | + foreach ( $file_field_names as $file_field_name ) { | |
| 2328 | + if ( isset( $result[ $file_field_name ] ) && ! isset( $uploads_result[ $file_field_name ] ) ) { | |
| 2329 | + unset( $result[ $file_field_name ] ); | |
| 2330 | + } | |
| 2331 | + } | |
| 2332 | + | |
| 2128 | 2333 | $result = array_merge( $result, $uploads_result ); |
| 2129 | 2334 | |
| 2130 | 2335 | $args = array( |
| 2131 | 2336 | 'ID' => get_current_user_id(), |
| @@ -2424,17 +2629,24 @@ | ||
| 2424 | 2629 | } |
| 2425 | 2630 | |
| 2426 | 2631 | $unlink_file = untrailingslashit( $upload_path ) . '/' . trim( $value, '/\\' ); |
| 2427 | 2632 | |
| 2428 | - if ( is_file( $unlink_file ) && file_exists( $unlink_file ) ) { | |
| 2429 | - wp_delete_file( $unlink_file ); | |
| 2633 | + // Canonicalize and enforce containment inside the uploads directory before deleting. | |
| 2634 | + $real_upload_path = realpath( $upload_path ); | |
| 2635 | + $real_unlink_file = realpath( $unlink_file ); | |
| 2430 | 2636 | |
| 2637 | + if ( $real_upload_path && $real_unlink_file && is_file( $real_unlink_file ) | |
| 2638 | + && strpos( $real_unlink_file, $real_upload_path . DIRECTORY_SEPARATOR ) === 0 ) { | |
| 2639 | + wp_delete_file( $real_unlink_file ); | |
| 2640 | + | |
| 2431 | 2641 | // For avatar/banner, also remove the original (non-thumb) file. |
| 2432 | 2642 | if ( $type ) { |
| 2433 | - $unlink_ori_file = str_replace( '_uwp_' . $type . '_thumb' . '.', '.', $unlink_file ); | |
| 2643 | + $unlink_ori_file = str_replace( '_uwp_' . $type . '_thumb' . '.', '.', $real_unlink_file ); | |
| 2644 | + $real_unlink_ori_file = realpath( $unlink_ori_file ); | |
| 2434 | 2645 | |
| 2435 | - if ( is_file( $unlink_ori_file ) && file_exists( $unlink_ori_file ) ) { | |
| 2436 | - wp_delete_file( $unlink_ori_file ); | |
| 2646 | + if ( $real_unlink_ori_file && is_file( $real_unlink_ori_file ) | |
| 2647 | + && strpos( $real_unlink_ori_file, $real_upload_path . DIRECTORY_SEPARATOR ) === 0 ) { | |
| 2648 | + wp_delete_file( $real_unlink_ori_file ); | |
| 2437 | 2649 | } |
| 2438 | 2650 | } |
| 2439 | 2651 | } |
| 2440 | 2652 | } |
| @@ -3917,17 +4129,26 @@ | ||
| 3917 | 4129 | $site_title = uwp_get_form_label( $field ); |
| 3918 | 4130 | $required_msg = ( ! empty( $field->is_required ) && $field->required_msg != '') ? __( stripslashes( $field->required_msg ), 'userswp' ) : ''; |
| 3919 | 4131 | $validation_text = ! empty( $field->validation_msg ) ? __( stripslashes( $field->validation_msg ), 'userswp' ) : ''; |
| 3920 | 4132 | |
| 4133 | + $is_forgot_email = ( $form_type === 'forgot' && $field->htmlvar_name === 'email' ); | |
| 4134 | + $input_type = $is_forgot_email ? 'text' : 'email'; | |
| 4135 | + if ( $is_forgot_email ) { | |
| 4136 | + $site_title = __( 'Username or Email', 'userswp' ); | |
| 4137 | + $placeholder = $site_title . ( ! empty( $field->is_required ) ? ' *' : '' ); | |
| 4138 | + } else { | |
| 4139 | + $placeholder = uwp_get_field_placeholder( $field ); | |
| 4140 | + } | |
| 4141 | + | |
| 3921 | 4142 | if ( $design_style ) { |
| 3922 | 4143 | $required = ! empty( $field->is_required ) ? ' <span class="text-danger">*</span>' : ''; |
| 3923 | 4144 | |
| 3924 | 4145 | echo aui()->input( |
| 3925 | 4146 | array( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped |
| 3926 | - 'type' => 'email', | |
| 4147 | + 'type' => $input_type, | |
| 3927 | 4148 | 'id' => esc_attr( $field->htmlvar_name ), |
| 3928 | 4149 | 'name' => esc_attr( $field->htmlvar_name ), |
| 3929 | - 'placeholder' => esc_attr( uwp_get_field_placeholder( $field ) ), | |
| 4150 | + 'placeholder' => esc_attr( $placeholder ), | |
| 3930 | 4151 | 'title' => esc_html( $site_title ), |
| 3931 | 4152 | 'value' => esc_attr( wp_unslash( $value ) ), |
| 3932 | 4153 | 'required' => (bool) $field->is_required, |
| 3933 | 4154 | 'validation_text' => $validation_text != '' ? esc_attr( $validation_text ) : esc_attr( $required_msg ), |
| @@ -3963,9 +4184,9 @@ | ||
| 3963 | 4184 | |
| 3964 | 4185 | <input name="<?php echo esc_attr( $field->htmlvar_name ); ?>" |
| 3965 | 4186 | class="<?php echo esc_attr( $field->css_class ); ?> uwp_textfield <?php echo esc_attr( $bs_form_control ); ?>" |
| 3966 | 4187 | id="<?php echo esc_attr( $field->htmlvar_name ); ?>" |
| 3967 | - placeholder="<?php echo esc_attr( uwp_get_field_placeholder( $field ) ); ?>" | |
| 4188 | + placeholder="<?php echo esc_attr( $placeholder ); ?>" | |
| 3968 | 4189 | value="<?php echo esc_attr( stripslashes( $value ) ); ?>" |
| 3969 | 4190 | title="<?php echo esc_attr( $site_title ); ?>" |
| 3970 | 4191 | <?php |
| 3971 | 4192 | if ( $field->is_required == 1 ) { |
| @@ -3971,9 +4192,9 @@ | ||
| 3971 | 4192 | if ( $field->is_required == 1 ) { |
| 3972 | 4193 | echo 'required="required"'; |
| 3973 | 4194 | } |
| 3974 | 4195 | ?> |
| 3975 | - type="email" | |
| 4196 | + type="<?php echo esc_attr( $input_type ); ?>" | |
| 3976 | 4197 | /> |
| 3977 | 4198 | <span class="uwp_message_note"><?php echo wp_kses_post( uwp_get_field_description( $field ) ); ?></span> |
| 3978 | 4199 | <?php if ( $field->is_required ) { ?> |
| 3979 | 4200 | <span class="uwp_message_error invalid-feedback"><?php echo esc_html__( stripslashes( $field->required_msg ), 'userswp' ); ?></span> |