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 ); } } }