Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 121 additions & 0 deletions assets/js/wu-password-reset.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/* global jQuery, wp, wu_password_reset */
/**
* Password strength meter for the password reset form.
*
* @since 2.3.0
*/
(function($) {
'use strict';

var i18n = typeof wu_password_reset !== 'undefined' ? wu_password_reset : {
enter_password: 'Enter a password',
short: 'Very weak',
weak: 'Weak',
medium: 'Medium',
strong: 'Strong',
mismatch: 'Passwords do not match'
};

var isPasswordValid = false;
var minStrength = typeof wu_password_reset !== 'undefined' ? parseInt(wu_password_reset.min_strength, 10) : 3;

/**
* Check password strength and update the UI.
*/
function checkPasswordStrength($pass1, $pass2, $result, $submit) {
var pass1 = $pass1.val();
var pass2 = $pass2.val();

// Reset classes
$result.attr('class', 'wu-py-2 wu-px-4 wu-block wu-text-sm wu-border-solid wu-border wu-mt-2');

if (!pass1) {
$result.addClass('wu-bg-gray-100 wu-border-gray-200').html(i18n.enter_password);
isPasswordValid = false;
$submit.prop('disabled', true);
return;
}

// Get disallowed list from WordPress
var disallowedList = '';
if (typeof wp !== 'undefined' && typeof wp.passwordStrength !== 'undefined') {
disallowedList = typeof wp.passwordStrength.userInputDisallowedList === 'undefined'
? wp.passwordStrength.userInputBlacklist()
: wp.passwordStrength.userInputDisallowedList();
}

var strength = wp.passwordStrength.meter(pass1, disallowedList, pass2);

isPasswordValid = false;

switch (strength) {
case 0:
case 1:
$result.addClass('wu-bg-red-200 wu-border-red-300').html(i18n.short);
break;
case 2:
$result.addClass('wu-bg-red-200 wu-border-red-300').html(i18n.weak);
break;
case 3:
$result.addClass('wu-bg-yellow-200 wu-border-yellow-300').html(i18n.medium);
if (minStrength <= 3) {
isPasswordValid = true;
}
break;
case 4:
$result.addClass('wu-bg-green-200 wu-border-green-300').html(i18n.strong);
isPasswordValid = true;
break;
case 5:
$result.addClass('wu-bg-red-200 wu-border-red-300').html(i18n.mismatch);
break;
default:
$result.addClass('wu-bg-red-200 wu-border-red-300').html(i18n.short);
}

$submit.prop('disabled', !isPasswordValid);
}

/**
* Initialize the password strength meter.
*/
$(document).ready(function() {
var $pass1 = $('#field-pass1');
var $pass2 = $('#field-pass2');
var $submit = $('#wp-submit');

if (!$pass1.length) {
return;
}

// Create strength meter element if it doesn't exist
var $result = $('#pass-strength-result');
if (!$result.length) {
$result = $('<div id="pass-strength-result" class="wu-py-2 wu-px-4 wu-bg-gray-100 wu-block wu-text-sm wu-border-solid wu-border wu-border-gray-200 wu-mt-2">' + i18n.enter_password + '</div>');
$pass1.after($result);
}

// Bind events
$pass1.on('keyup input', function() {
checkPasswordStrength($pass1, $pass2, $result, $submit);
});
$pass2.on('keyup input', function() {
checkPasswordStrength($pass1, $pass2, $result, $submit);
});

// Disable submit initially
$submit.prop('disabled', true);

// Prevent form submission if password is too weak
$pass1.closest('form').on('submit', function(e) {
if (!isPasswordValid) {
e.preventDefault();
return false;
}
});

// Initial check
checkPasswordStrength($pass1, $pass2, $result, $submit);
});

})(jQuery);
94 changes: 94 additions & 0 deletions inc/checkout/class-checkout-pages.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@

add_action('lost_password', [$this, 'maybe_handle_password_reset_errors']);

add_action('validate_password_reset', [$this, 'validate_password_strength'], 5, 2);

add_action('validate_password_reset', [$this, 'maybe_handle_password_reset_errors']);

/**
Expand Down Expand Up @@ -204,6 +206,9 @@
'password_reset_mismatch' => __('<strong>Error:</strong> The passwords do not match.'), // phpcs:ignore WordPress.WP.I18n.MissingArgDomain
'invalidkey' => __('<strong>Error:</strong> Your password reset link appears to be invalid. Please request a new link below.'), // phpcs:ignore WordPress.WP.I18n.MissingArgDomain
'expiredkey' => __('<strong>Error:</strong> Your password reset link has expired. Please request a new link below.'), // phpcs:ignore WordPress.WP.I18n.MissingArgDomain
'invalid_key' => __('<strong>Error:</strong> Your password reset link appears to be invalid. Please request a new link below.'), // phpcs:ignore WordPress.WP.I18n.MissingArgDomain
'expired_key' => __('<strong>Error:</strong> Your password reset link has expired. Please request a new link below.'), // phpcs:ignore WordPress.WP.I18n.MissingArgDomain
'password_too_weak' => __('<strong>Error:</strong> Please choose a stronger password. The password must be at least medium strength.', 'ultimate-multisite'),
];

/**
Expand Down Expand Up @@ -247,6 +252,95 @@
}
}

/**
* Validate password strength during password reset.
*
* Enforces a minimum password strength of "medium" (strength level 3).
*
* @since 2.3.0
*
* @param \WP_Error $errors The error object.
* @param \WP_User $user The user object.
* @return void
*/
public function validate_password_strength($errors, $user): void {

Check warning on line 266 in inc/checkout/class-checkout-pages.php

View workflow job for this annotation

GitHub Actions / Code Quality Checks

The method parameter $user is never used

$password = wu_request('pass1', '');

if (empty($password)) {
return;
}

// Use WordPress password strength meter.
// Strength levels: 0-1 = very weak, 2 = weak, 3 = medium, 4 = strong.
// We require at least medium (3).
$strength = $this->calculate_password_strength($password);

if ($strength < 3) {
$errors->add('password_too_weak', __('<strong>Error:</strong> Please choose a stronger password. The password must be at least medium strength.', 'ultimate-multisite'));
}
}

/**
* Calculate password strength using similar logic to WordPress.
*
* @since 2.3.0
*
* @param string $password The password to check.
* @return int Strength level: 0-1 = very weak, 2 = weak, 3 = medium, 4 = strong.
*/
protected function calculate_password_strength(string $password): int {

$strength = 0;
$length = strlen($password);

// Minimum length check
if ($length < 6) {
return 0;
}

// Length scoring
if ($length >= 8) {
++$strength;
}

if ($length >= 12) {
++$strength;
}

// Character variety scoring
if (preg_match('/[a-z]/', $password)) {
++$strength;
}

if (preg_match('/[A-Z]/', $password)) {
++$strength;
}

if (preg_match('/[0-9]/', $password)) {
++$strength;
}

if (preg_match('/[^a-zA-Z0-9]/', $password)) {
++$strength;
}

// Map to WordPress strength levels (0-4)
if ($strength <= 1) {
return 1; // Very weak
}

if ($strength <= 2) {
return 2; // Weak
}

if ($strength <= 4) {
return 3; // Medium
}

return 4; // Strong
}

/**
* Maybe redirects users to the confirm screen.
*
Expand Down
80 changes: 67 additions & 13 deletions inc/ui/class-login-form-element.php
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,33 @@ public function fields() {
public function register_scripts(): void {

wp_enqueue_style('wu-admin');

// Enqueue password strength meter for reset password page.
if ($this->is_reset_password_page()) {
wp_enqueue_script('password-strength-meter');

wp_enqueue_script(
'wu-password-reset',
wu_get_asset('wu-password-reset.js', 'js'),
['jquery', 'password-strength-meter'],
wu_get_version(),
true
);

wp_localize_script(
'wu-password-reset',
'wu_password_reset',
[
'enter_password' => __('Enter a password', 'ultimate-multisite'),
'short' => __('Very weak', 'ultimate-multisite'),
'weak' => __('Weak', 'ultimate-multisite'),
'medium' => __('Medium', 'ultimate-multisite'),
'strong' => __('Strong', 'ultimate-multisite'),
'mismatch' => __('Passwords do not match', 'ultimate-multisite'),
'min_strength' => 3, // Minimum strength level required (3 = medium).
]
);
}
}

/**
Expand Down Expand Up @@ -605,7 +632,36 @@ public function output($atts, $content = null): void {

$user = check_password_reset_key($rp_key, $rp_login);

if (isset($_POST['pass1']) && isset($_POST['rp_key']) && ! hash_equals(wp_unslash($_POST['rp_key']), wp_unslash($_POST['rp_key']))) { // phpcs:ignore WordPress.Security.NonceVerification
// If the reset key is invalid or expired, redirect with appropriate error.
if (is_wp_error($user)) {
$error_code = $user->get_error_code();
$redirect_to = add_query_arg(
[
'action' => 'lostpassword',
'error' => $error_code,
],
remove_query_arg(['action', 'key', 'login'])
);

// Clear the invalid cookie.
setcookie(
$rp_cookie,
' ',
[
'expires' => time() - YEAR_IN_SECONDS,
'path' => '/',
'domain' => (string) COOKIE_DOMAIN,
'secure' => is_ssl(),
'httponly' => true,
]
);

wp_safe_redirect($redirect_to);

exit;
}

if (isset($_POST['pass1']) && isset($_POST['rp_key']) && ! hash_equals($rp_key, wp_unslash($_POST['rp_key']))) { // phpcs:ignore WordPress.Security.NonceVerification
$user = false;
}
} else {
Expand All @@ -615,48 +671,46 @@ public function output($atts, $content = null): void {
$redirect_to = add_query_arg('password-reset', 'success', remove_query_arg(['action', 'error']));

$fields = [
'pass1' => [
'pass1' => [
'type' => 'password',
'title' => __('New password'), // phpcs:ignore WordPress.WP.I18n.MissingArgDomain
'placeholder' => '',
'value' => '',
'meter' => true,
'html_attr' => [
'size' => 24,
'autocapitalize' => 'off',
'autocomplete' => 'new-password',
],
],
'pass2' => [
'pass2' => [
'type' => 'password',
'title' => __('Confirm new password'), // phpcs:ignore WordPress.WP.I18n.MissingArgDomain
'placeholder' => '',
'value' => '',
'html_attr' => [
'size' => 24,
'autocapitalize' => 'off',
'autocomplete' => 'new-password',
],
],
'lost-password-instructions' => [
'type' => 'note',
'desc' => wp_get_password_hint(),
'tooltip' => '',
],
'action' => [
'action' => [
'type' => 'hidden',
'value' => 'resetpass',
],
'rp_key' => [
'rp_key' => [
'type' => 'hidden',
'value' => $rp_key,
],
'user_login' => [
'user_login' => [
'type' => 'hidden',
'value' => $rp_login,
],
'redirect_to' => [
'redirect_to' => [
'type' => 'hidden',
'value' => $redirect_to,
],
'wp-submit' => [
'wp-submit' => [
'type' => 'submit',
'title' => __('Save Password'), // phpcs:ignore WordPress.WP.I18n.MissingArgDomain
'value' => __('Save Password'), // phpcs:ignore WordPress.WP.I18n.MissingArgDomain
Expand Down
Loading
Loading