Automattic\WooCommerce\Internal\StockNotifications\Frontend
NotificationManagementService{} │ WC 1.0
Notification management service.
No Hooks.
Usage
$NotificationManagementService = new NotificationManagementService(); // use class methods
Methods
- public get_resend_verification_email_url( Notification $notification )
- public init( EmailManager $email_manager )
- public maybe_process_resend_request()
- public resend_verification_email( Notification $notification )
- private ensure_notice_session()
NotificationManagementService{} NotificationManagementService{} code WC 11.2.1
class NotificationManagementService {
/**
* Query argument carrying the notification id for a resend-verification request.
*/
public const RESEND_QUERY_ARG = 'wc_bis_resend_notification';
/**
* Nonce action for resend-verification URLs.
*/
public const RESEND_NONCE_ACTION = 'wc_bis_resend_verification_email_nonce';
/**
* Meta key tracking the last time a verification email was dispatched.
*
* Used to rate-limit the frontend resend endpoint.
*/
public const LAST_VERIFY_EMAIL_SENT_META = '_last_verify_email_sent_at';
/**
* Minimum seconds between back-to-back resend requests.
*/
public const RESEND_RATE_LIMIT_SECONDS = 60;
/**
* Error code returned by {@see self::resend_verification_email()} when the notification is no longer pending.
*/
public const RESEND_ERROR_NOT_PENDING = 'wc_bis_resend_not_pending';
/**
* Error code returned by {@see self::resend_verification_email()} when a send happened too recently.
*/
public const RESEND_ERROR_RATE_LIMITED = 'wc_bis_resend_rate_limited';
/**
* Email manager.
*
* @var EmailManager
*/
private EmailManager $email_manager;
/**
* Init the service.
*
* @internal
*
* @param EmailManager $email_manager The email manager.
*/
final public function init( EmailManager $email_manager ): void {
$this->email_manager = $email_manager;
add_action( 'template_redirect', array( $this, 'maybe_process_resend_request' ) );
}
/**
* Get resend verification email URL.
*
* @param Notification $notification The notification.
* @return string The resend verification email URL.
*/
public function get_resend_verification_email_url( Notification $notification ): string {
$url = add_query_arg(
array(
self::RESEND_QUERY_ARG => $notification->get_id(),
),
$notification->get_product_permalink()
);
return wp_nonce_url( $url, self::RESEND_NONCE_ACTION . '_' . $notification->get_id() );
}
/**
* Handle the resend-verification request if the current request carries one.
*/
public function maybe_process_resend_request(): void {
// Only run on frontend GET requests — skip admin, POST, CLI, etc. before doing any nonce/DB work.
if ( is_admin() ) {
return;
}
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.NonceVerification.Recommended
$method = isset( $_SERVER['REQUEST_METHOD'] ) ? strtoupper( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) ) : 'GET';
if ( 'GET' !== $method ) {
return;
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( ! isset( $_GET[ self::RESEND_QUERY_ARG ] ) ) {
return;
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$notification_id = absint( wp_unslash( $_GET[ self::RESEND_QUERY_ARG ] ) );
$nonce = isset( $_GET['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ) : '';
if ( empty( $notification_id ) ) {
return;
}
// Scope the nonce per-notification so one valid resend URL cannot be replayed
// across the notification id query arg to trigger emails for other customers.
if ( ! wp_verify_nonce( $nonce, self::RESEND_NONCE_ACTION . '_' . $notification_id ) ) {
return;
}
$notification = Factory::get_notification( $notification_id );
if ( ! $notification instanceof Notification ) {
return;
}
// Only the owner may resend a customer-linked notification. Bail silently on a
// mismatch so the response doesn't confirm that the id exists.
$owner_id = (int) $notification->get_user_id();
if ( $owner_id > 0 && is_user_logged_in() && get_current_user_id() !== $owner_id ) {
return;
}
$this->ensure_notice_session();
$redirect_url = $notification->get_product_permalink();
if ( empty( $redirect_url ) ) {
$redirect_url = wc_get_page_permalink( 'shop' );
}
$result = $this->resend_verification_email( $notification );
if ( is_wp_error( $result ) ) {
$notice_type = self::RESEND_ERROR_RATE_LIMITED === $result->get_error_code() ? 'notice' : 'error';
wc_add_notice( esc_html( $result->get_error_message() ), $notice_type );
wp_safe_redirect( $redirect_url );
exit;
}
/* translators: %s user email. */
wc_add_notice( sprintf( esc_html__( 'Verification email sent to %s.', 'woocommerce' ), $notification->get_user_email() ), 'success' );
wp_safe_redirect( $redirect_url );
exit;
}
/**
* Send the verification email for a pending notification again.
*
* Pure domain step: checks the status and the rate limit, records the send time,
* and dispatches the email. Callers own authentication, nonces, notices and redirects.
*
* @param Notification $notification The notification to resend for.
* @return true|\WP_Error True on send, or an error carrying one of the `RESEND_ERROR_*` codes.
*/
public function resend_verification_email( Notification $notification ) {
if ( NotificationStatus::PENDING !== $notification->get_status() ) {
return new \WP_Error( self::RESEND_ERROR_NOT_PENDING, __( 'This notification is already verified or cancelled.', 'woocommerce' ) );
}
$last_sent_at = (int) $notification->get_meta( self::LAST_VERIFY_EMAIL_SENT_META );
if ( $last_sent_at > 0 && ( time() - $last_sent_at ) < self::RESEND_RATE_LIMIT_SECONDS ) {
return new \WP_Error( self::RESEND_ERROR_RATE_LIMITED, __( 'Please wait a moment before requesting another verification email.', 'woocommerce' ) );
}
// Persist the rate-limit timestamp before dispatching the email so two near-simultaneous
// requests can't both pass the rate-limit check and trigger duplicate sends.
$notification->update_meta_data( self::LAST_VERIFY_EMAIL_SENT_META, (string) time() );
$notification->save();
$this->email_manager->send_verify_email( $notification );
return true;
}
/**
* Ensure there is a cookie-based session so frontend notices survive the redirect.
*/
private function ensure_notice_session(): void {
if ( WC()->session instanceof \WC_Session_Handler && ! WC()->session->has_session() ) {
WC()->session->set_customer_session_cookie( true );
}
}
}