diff --git a/assets/js/wu-password-reset.js b/assets/js/wu-password-reset.js
new file mode 100644
index 00000000..94de86e9
--- /dev/null
+++ b/assets/js/wu-password-reset.js
@@ -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 = $('
' + i18n.enter_password + '
');
+ $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);
diff --git a/inc/checkout/class-checkout-pages.php b/inc/checkout/class-checkout-pages.php
index 3c902aa6..709b54b6 100644
--- a/inc/checkout/class-checkout-pages.php
+++ b/inc/checkout/class-checkout-pages.php
@@ -62,6 +62,8 @@ public function init(): void {
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']);
/**
@@ -204,6 +206,9 @@ public function get_error_message($error_code, $username = '') {
'password_reset_mismatch' => __('Error: The passwords do not match.'), // phpcs:ignore WordPress.WP.I18n.MissingArgDomain
'invalidkey' => __('Error: Your password reset link appears to be invalid. Please request a new link below.'), // phpcs:ignore WordPress.WP.I18n.MissingArgDomain
'expiredkey' => __('Error: Your password reset link has expired. Please request a new link below.'), // phpcs:ignore WordPress.WP.I18n.MissingArgDomain
+ 'invalid_key' => __('Error: Your password reset link appears to be invalid. Please request a new link below.'), // phpcs:ignore WordPress.WP.I18n.MissingArgDomain
+ 'expired_key' => __('Error: Your password reset link has expired. Please request a new link below.'), // phpcs:ignore WordPress.WP.I18n.MissingArgDomain
+ 'password_too_weak' => __('Error: Please choose a stronger password. The password must be at least medium strength.', 'ultimate-multisite'),
];
/**
@@ -247,6 +252,95 @@ public function maybe_handle_password_reset_errors($errors): void {
}
}
+ /**
+ * 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 {
+
+ $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', __('Error: 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.
*
diff --git a/inc/ui/class-login-form-element.php b/inc/ui/class-login-form-element.php
index bc3a3574..dedbeb71 100644
--- a/inc/ui/class-login-form-element.php
+++ b/inc/ui/class-login-form-element.php
@@ -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).
+ ]
+ );
+ }
}
/**
@@ -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 {
@@ -615,17 +671,19 @@ 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' => '',
@@ -633,30 +691,26 @@ public function output($atts, $content = null): void {
'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
diff --git a/views/admin-pages/fields/field-password.php b/views/admin-pages/fields/field-password.php
new file mode 100644
index 00000000..40d9f964
--- /dev/null
+++ b/views/admin-pages/fields/field-password.php
@@ -0,0 +1,64 @@
+
+print_wrapper_html_attributes(); ?>>
+
+
+
+ $field,
+ ]
+ );
+
+ ?>
+
+ print_html_attributes(); ?>>
+
+ meter)) : ?>
+
+
+
+
+
+
+
+ $field,
+ ]
+ );
+
+ ?>
+
+
+
+