Commit 8b9abdaa70e for woocommerce

commit 8b9abdaa70ebec1c56e5b9bc65340199cc802915
Author: Mike Jolley <mike.jolley@me.com>
Date:   Thu Jul 2 16:14:27 2026 +0100

    Add resend action to the temporary-password My Account notice (#65821)

    * Add resend action to the temporary-password My Account notice

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

    * Load mailer before firing reset-password notification on resend

    The resend handler fired woocommerce_reset_password_notification without
    loading the email classes first, so no listener was registered and no
    email was sent. Mirror the lost-password flow by calling WC()->mailer()
    before the action.

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

    * Move mailer-load comment to its own line (PHPCS PostStatementComment)

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

    * Rate-limit set-password resends and suppress notice during cooldown

    * Delete resend rate-limit meta when the password nag is cleared

    Once the customer sets their password the temporary-password notice is
    gone for good, so the resend cooldown timestamp is dead weight. Drop it
    alongside the default_password_nag reset.

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

    ---------

    Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

diff --git a/plugins/woocommerce/changelog/add-resend-set-password-link b/plugins/woocommerce/changelog/add-resend-set-password-link
new file mode 100644
index 00000000000..b72804543e7
--- /dev/null
+++ b/plugins/woocommerce/changelog/add-resend-set-password-link
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add a resend action to the temporary-password notice on My Account so customers can request a fresh change-password email.
diff --git a/plugins/woocommerce/includes/class-wc-form-handler.php b/plugins/woocommerce/includes/class-wc-form-handler.php
index daea46ee9ca..f6bfb58b2b7 100644
--- a/plugins/woocommerce/includes/class-wc-form-handler.php
+++ b/plugins/woocommerce/includes/class-wc-form-handler.php
@@ -16,11 +16,22 @@ defined( 'ABSPATH' ) || exit;
  */
 class WC_Form_Handler {

+	/**
+	 * User meta key tracking the last time the set-password link was resent. Used to rate-limit resends.
+	 */
+	const SET_PASSWORD_RESEND_META = '_wc_set_password_resend_at';
+
+	/**
+	 * Minimum seconds between back-to-back set-password resend requests.
+	 */
+	const SET_PASSWORD_RESEND_RATE_LIMIT_SECONDS = 60;
+
 	/**
 	 * Hook in methods.
 	 */
 	public static function init() {
 		add_action( 'template_redirect', array( __CLASS__, 'redirect_reset_password_link' ) );
+		add_action( 'template_redirect', array( __CLASS__, 'resend_set_password' ) );
 		add_action( 'template_redirect', array( __CLASS__, 'save_address' ) );
 		add_action( 'template_redirect', array( __CLASS__, 'save_account_details' ) );
 		add_action( 'wp_loaded', array( __CLASS__, 'checkout_action' ), 20 );
@@ -76,6 +87,59 @@ class WC_Form_Handler {
 		}
 	}

+	/**
+	 * Resend the change-password link to a logged-in customer who still has a temporary password.
+	 *
+	 * Triggered by the temporary-password notice on the My Account pages. Generates a fresh
+	 * password-reset key for the current user and dispatches the reset-password email, mirroring
+	 * the lost-password flow but for the already-authenticated user.
+	 *
+	 * @since 11.0.0
+	 */
+	public static function resend_set_password(): void {
+		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
+		if ( ! isset( $_GET['wc-resend-set-password'] ) || ! is_user_logged_in() ) {
+			return;
+		}
+
+		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
+		$nonce_value = isset( $_GET['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ) : '';
+
+		if ( ! wp_verify_nonce( $nonce_value, 'wc-resend-set-password' ) ) {
+			return;
+		}
+
+		$user     = wp_get_current_user();
+		$redirect = wc_get_page_permalink( 'myaccount' );
+
+		// Rate-limit resends so the button can't be used to spam the customer's inbox.
+		$last_sent_at = (int) get_user_meta( $user->ID, self::SET_PASSWORD_RESEND_META, true );
+		if ( $last_sent_at > 0 && ( time() - $last_sent_at ) < self::SET_PASSWORD_RESEND_RATE_LIMIT_SECONDS ) {
+			wc_add_notice( __( 'Please wait a moment before requesting another link to change your password.', 'woocommerce' ), 'notice' );
+			wp_safe_redirect( $redirect );
+			exit;
+		}
+
+		$key = get_password_reset_key( $user );
+
+		if ( is_wp_error( $key ) ) {
+			wc_add_notice( __( 'Sorry, we were unable to resend the link. Please try again.', 'woocommerce' ), 'error' );
+		} else {
+			// Persist the rate-limit timestamp before dispatching so two near-simultaneous requests can't both pass.
+			// This timestamp also suppresses the temporary-password notice during the cooldown — see
+			// WC_Shortcode_My_Account::my_account_add_notices() — so the confirmation below isn't contradicted.
+			update_user_meta( $user->ID, self::SET_PASSWORD_RESEND_META, (string) time() );
+			// Load email classes so the reset-password notification has a listener.
+			WC()->mailer();
+			// phpcs:ignore WooCommerce.Commenting.CommentHooks -- Re-fires woocommerce_reset_password_notification, documented in WC_Shortcode_My_Account::retrieve_password().
+			do_action( 'woocommerce_reset_password_notification', $user->user_login, $key );
+			wc_add_notice( __( 'We have emailed you a new link to change your password.', 'woocommerce' ) );
+		}
+
+		wp_safe_redirect( $redirect );
+		exit;
+	}
+
 	/**
 	 * Save and and update a billing or shipping address if the
 	 * form was submitted through the user account page.
diff --git a/plugins/woocommerce/includes/shortcodes/class-wc-shortcode-my-account.php b/plugins/woocommerce/includes/shortcodes/class-wc-shortcode-my-account.php
index cc081a9d3c8..4dbb81a8d56 100644
--- a/plugins/woocommerce/includes/shortcodes/class-wc-shortcode-my-account.php
+++ b/plugins/woocommerce/includes/shortcodes/class-wc-shortcode-my-account.php
@@ -90,16 +90,22 @@ class WC_Shortcode_My_Account {
 			wc_add_notice( sprintf( __( 'Are you sure you want to log out? <a href="%s">Confirm and log out</a>', 'woocommerce' ), wc_logout_url() ) );
 		}

-		if ( get_user_option( 'default_password_nag' ) && ( wc_is_current_account_menu_item( 'dashboard' ) || wc_is_current_account_menu_item( 'edit-account' ) ) ) {
+		// Suppress the nag during the resend cooldown so it doesn't contradict the "we emailed you" confirmation.
+		// Driven off the same timestamp WC_Form_Handler::resend_set_password() writes, so the notice reappears
+		// once the cooldown lapses and the link can be requested again.
+		$last_resend_at  = (int) get_user_meta( get_current_user_id(), WC_Form_Handler::SET_PASSWORD_RESEND_META, true );
+		$within_cooldown = $last_resend_at > 0 && ( time() - $last_resend_at ) < WC_Form_Handler::SET_PASSWORD_RESEND_RATE_LIMIT_SECONDS;
+
+		if ( ! $within_cooldown && get_user_option( 'default_password_nag' ) && ( wc_is_current_account_menu_item( 'dashboard' ) || wc_is_current_account_menu_item( 'edit-account' ) ) ) {
+			$resend_url = wp_nonce_url( add_query_arg( 'wc-resend-set-password', '1', wc_get_page_permalink( 'myaccount' ) ), 'wc-resend-set-password' );
 			wc_add_notice(
 				sprintf(
-					// translators: %s: site name.
-					__( 'Your account with %s is using a temporary password. We emailed you a link to change your password.', 'woocommerce' ),
-					esc_html( wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES ) )
-				),
-				'notice',
-				array(),
-				true
+					/* translators: %1$s and %2$s are opening and closing anchor tags for the resend-link button. */
+					__( '%1$sResend%2$s', 'woocommerce' ),
+					'<a href="' . esc_url( $resend_url ) . '" class="button wc-forward">',
+					'</a>'
+				) . ' ' . __( 'Your account is using a temporary password. We emailed you a link to change your password.', 'woocommerce' ),
+				'notice'
 			);
 		}
 	}
@@ -378,6 +384,8 @@ class WC_Shortcode_My_Account {

 		wp_set_password( $new_pass, $user->ID );
 		update_user_meta( $user->ID, 'default_password_nag', false );
+		// The temporary-password notice is gone for good now, so drop its resend rate-limit timestamp.
+		delete_user_meta( $user->ID, WC_Form_Handler::SET_PASSWORD_RESEND_META );

 		/**
 		 * Fires after the user's password has been reset via WooCommerce.
diff --git a/plugins/woocommerce/phpstan-baseline.neon b/plugins/woocommerce/phpstan-baseline.neon
index 5ac7528b465..a41c4e3b44a 100644
--- a/plugins/woocommerce/phpstan-baseline.neon
+++ b/plugins/woocommerce/phpstan-baseline.neon
@@ -31665,12 +31665,6 @@ parameters:
 			count: 4
 			path: includes/shortcodes/class-wc-shortcode-my-account.php

-		-
-			message: '#^Function wc_add_notice invoked with 4 parameters, 1\-3 required\.$#'
-			identifier: arguments.count
-			count: 1
-			path: includes/shortcodes/class-wc-shortcode-my-account.php
-
 		-
 			message: '#^Method WC_Shortcode_My_Account\:\:add_payment_method\(\) has no return type specified\.$#'
 			identifier: missingType.return