I’ll build this as a production-style inc/ajax/newsletter.php module for your RX theme: secure AJAX handlers, validation, nonce checks, database table, admin helpers, unsubscribe, double opt-in, rate limiting, logging, and extensible hooks.
Below is an advanced inc/ajax/newsletter.php file for your RX Theme. It includes many useful newsletter features: subscribe, unsubscribe, double opt-in confirmation, nonce security, rate limiting, database table creation, admin stats, CSV export, privacy-safe logging, honeypot spam protection, consent checking, and WordPress hooks.
Create this file:
<?php
/**
* RX Theme Newsletter AJAX System
*
* File: inc/ajax/newsletter.php
*
* Features:
* - Secure AJAX newsletter subscribe
* - Guest + logged-in support
* - Nonce verification
* - Honeypot anti-spam
* - Email validation and sanitization
* - Name sanitization
* - Consent/GDPR checkbox validation
* - Rate limiting by IP and email
* - Custom database table
* - Double opt-in email confirmation
* - Unsubscribe link support
* - Resubscribe support
* - Admin-only stats AJAX
* - Admin-only CSV export
* - Privacy-safe IP hashing
* - Extensible WordPress hooks/filters
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( ! class_exists( 'RX_Theme_Newsletter_Ajax' ) ) :
final class RX_Theme_Newsletter_Ajax {
/**
* Database version.
*/
const DB_VERSION = '1.0.0';
/**
* Option name for DB version.
*/
const DB_VERSION_OPTION = 'rx_newsletter_db_version';
/**
* AJAX nonce action.
*/
const NONCE_ACTION = 'rx_newsletter_nonce_action';
/**
* AJAX nonce field.
*/
const NONCE_FIELD = 'security';
/**
* Subscribe rate limit transient prefix.
*/
const RATE_PREFIX = 'rx_newsletter_rate_';
/**
* Email rate limit transient prefix.
*/
const EMAIL_RATE_PREFIX = 'rx_newsletter_email_rate_';
/**
* Instance.
*/
private static $instance = null;
/**
* Get instance.
*/
public static function instance() {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor.
*/
private function __construct() {
add_action( 'after_switch_theme', array( $this, 'maybe_create_table' ) );
add_action( 'init', array( $this, 'maybe_create_table' ) );
add_action( 'wp_ajax_rx_newsletter_subscribe', array( $this, 'ajax_subscribe' ) );
add_action( 'wp_ajax_nopriv_rx_newsletter_subscribe', array( $this, 'ajax_subscribe' ) );
add_action( 'wp_ajax_rx_newsletter_unsubscribe', array( $this, 'ajax_unsubscribe' ) );
add_action( 'wp_ajax_nopriv_rx_newsletter_unsubscribe', array( $this, 'ajax_unsubscribe' ) );
add_action( 'wp_ajax_rx_newsletter_confirm', array( $this, 'ajax_confirm' ) );
add_action( 'wp_ajax_nopriv_rx_newsletter_confirm', array( $this, 'ajax_confirm' ) );
add_action( 'wp_ajax_rx_newsletter_stats', array( $this, 'ajax_stats' ) );
add_action( 'wp_ajax_rx_newsletter_export_csv', array( $this, 'ajax_export_csv' ) );
add_shortcode( 'rx_newsletter_form', array( $this, 'shortcode_form' ) );
}
/**
* Get table name.
*/
public function table_name() {
global $wpdb;
return $wpdb->prefix . 'rx_newsletter_subscribers';
}
/**
* Create or update newsletter table.
*/
public function maybe_create_table() {
$current_version = get_option( self::DB_VERSION_OPTION );
if ( self::DB_VERSION === $current_version ) {
return;
}
global $wpdb;
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
$table_name = $this->table_name();
$charset_collate = $wpdb->get_charset_collate();
$sql = "CREATE TABLE {$table_name} (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
email VARCHAR(190) NOT NULL,
name VARCHAR(190) NULL,
status VARCHAR(30) NOT NULL DEFAULT 'pending',
source VARCHAR(190) NULL,
consent TINYINT(1) NOT NULL DEFAULT 0,
confirm_token VARCHAR(128) NULL,
unsubscribe_token VARCHAR(128) NULL,
ip_hash VARCHAR(128) NULL,
user_agent TEXT NULL,
referrer TEXT NULL,
meta LONGTEXT NULL,
confirmed_at DATETIME NULL,
unsubscribed_at DATETIME NULL,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY email (email),
KEY status (status),
KEY confirm_token (confirm_token),
KEY unsubscribe_token (unsubscribe_token),
KEY created_at (created_at)
) {$charset_collate};";
dbDelta( $sql );
update_option( self::DB_VERSION_OPTION, self::DB_VERSION, false );
}
/**
* Newsletter shortcode form.
*
* Usage:
* [rx_newsletter_form]
*/
public function shortcode_form( $atts = array() ) {
$atts = shortcode_atts(
array(
'title' => __( 'Subscribe to Newsletter', 'rx-theme' ),
'description' => __( 'Get latest health articles, updates, and useful resources.', 'rx-theme' ),
'button' => __( 'Subscribe', 'rx-theme' ),
'source' => 'shortcode',
),
$atts,
'rx_newsletter_form'
);
ob_start();
?>
<div class="rx-newsletter-box" data-rx-newsletter>
<h3 class="rx-newsletter-title"><?php echo esc_html( $atts['title'] ); ?></h3>
<p class="rx-newsletter-description">
<?php echo esc_html( $atts['description'] ); ?>
</p>
<form class="rx-newsletter-form" method="post">
<input type="hidden" name="action" value="rx_newsletter_subscribe">
<input type="hidden" name="security" value="<?php echo esc_attr( wp_create_nonce( self::NONCE_ACTION ) ); ?>">
<input type="hidden" name="source" value="<?php echo esc_attr( $atts['source'] ); ?>">
<p class="rx-newsletter-field">
<label for="rx-newsletter-name"><?php esc_html_e( 'Name', 'rx-theme' ); ?></label>
<input id="rx-newsletter-name" type="text" name="name" placeholder="<?php esc_attr_e( 'Your name', 'rx-theme' ); ?>" autocomplete="name">
</p>
<p class="rx-newsletter-field">
<label for="rx-newsletter-email"><?php esc_html_e( 'Email address', 'rx-theme' ); ?></label>
<input id="rx-newsletter-email" type="email" name="email" placeholder="<?php esc_attr_e( 'you@example.com', 'rx-theme' ); ?>" autocomplete="email" required>
</p>
<p class="rx-newsletter-field rx-newsletter-consent">
<label>
<input type="checkbox" name="consent" value="1" required>
<?php esc_html_e( 'I agree to receive newsletter emails and understand I can unsubscribe anytime.', 'rx-theme' ); ?>
</label>
</p>
<p class="rx-newsletter-hp" style="display:none !important;">
<label>
<?php esc_html_e( 'Leave this field empty', 'rx-theme' ); ?>
<input type="text" name="website_url" value="" tabindex="-1" autocomplete="off">
</label>
</p>
<button type="submit" class="rx-newsletter-submit">
<?php echo esc_html( $atts['button'] ); ?>
</button>
<div class="rx-newsletter-response" aria-live="polite"></div>
</form>
</div>
<?php
return ob_get_clean();
}
/**
* AJAX subscribe.
*/
public function ajax_subscribe() {
$this->verify_nonce();
$this->maybe_create_table();
$honeypot = isset( $_POST['website_url'] ) ? sanitize_text_field( wp_unslash( $_POST['website_url'] ) ) : '';
if ( ! empty( $honeypot ) ) {
$this->log_security_event( 'honeypot_triggered' );
wp_send_json_error(
array(
'message' => __( 'Subscription could not be completed.', 'rx-theme' ),
),
400
);
}
$email = isset( $_POST['email'] ) ? sanitize_email( wp_unslash( $_POST['email'] ) ) : '';
$name = isset( $_POST['name'] ) ? sanitize_text_field( wp_unslash( $_POST['name'] ) ) : '';
$source = isset( $_POST['source'] ) ? sanitize_key( wp_unslash( $_POST['source'] ) ) : 'unknown';
$consent = isset( $_POST['consent'] ) ? absint( $_POST['consent'] ) : 0;
if ( empty( $email ) || ! is_email( $email ) ) {
wp_send_json_error(
array(
'message' => __( 'Please enter a valid email address.', 'rx-theme' ),
),
422
);
}
if ( ! $consent ) {
wp_send_json_error(
array(
'message' => __( 'Please accept the newsletter consent checkbox.', 'rx-theme' ),
),
422
);
}
if ( $this->is_disposable_email( $email ) ) {
wp_send_json_error(
array(
'message' => __( 'Please use a valid personal or business email address.', 'rx-theme' ),
),
422
);
}
if ( $this->is_rate_limited( $email ) ) {
wp_send_json_error(
array(
'message' => __( 'Too many attempts. Please try again later.', 'rx-theme' ),
),
429
);
}
/**
* Filter newsletter subscription data before saving.
*/
$subscription_data = apply_filters(
'rx_newsletter_subscription_data',
array(
'email' => strtolower( $email ),
'name' => $name,
'source' => $source,
'consent' => $consent,
)
);
$result = $this->save_subscriber( $subscription_data );
if ( is_wp_error( $result ) ) {
wp_send_json_error(
array(
'message' => $result->get_error_message(),
),
400
);
}
$subscriber = $result;
$this->send_confirm_email( $subscriber );
do_action( 'rx_newsletter_after_subscribe', $subscriber );
wp_send_json_success(
array(
'message' => __( 'Almost done. Please check your email and confirm your subscription.', 'rx-theme' ),
'status' => $subscriber['status'],
)
);
}
/**
* AJAX unsubscribe.
*/
public function ajax_unsubscribe() {
$this->verify_nonce();
$email = isset( $_POST['email'] ) ? sanitize_email( wp_unslash( $_POST['email'] ) ) : '';
$token = isset( $_POST['token'] ) ? sanitize_text_field( wp_unslash( $_POST['token'] ) ) : '';
if ( empty( $email ) || ! is_email( $email ) ) {
wp_send_json_error(
array(
'message' => __( 'Please enter a valid email address.', 'rx-theme' ),
),
422
);
}
$result = $this->unsubscribe( $email, $token );
if ( is_wp_error( $result ) ) {
wp_send_json_error(
array(
'message' => $result->get_error_message(),
),
400
);
}
wp_send_json_success(
array(
'message' => __( 'You have been unsubscribed successfully.', 'rx-theme' ),
)
);
}
/**
* AJAX confirm double opt-in.
*/
public function ajax_confirm() {
$this->verify_nonce();
$email = isset( $_POST['email'] ) ? sanitize_email( wp_unslash( $_POST['email'] ) ) : '';
$token = isset( $_POST['token'] ) ? sanitize_text_field( wp_unslash( $_POST['token'] ) ) : '';
if ( empty( $email ) || ! is_email( $email ) || empty( $token ) ) {
wp_send_json_error(
array(
'message' => __( 'Invalid confirmation request.', 'rx-theme' ),
),
422
);
}
$result = $this->confirm_subscriber( $email, $token );
if ( is_wp_error( $result ) ) {
wp_send_json_error(
array(
'message' => $result->get_error_message(),
),
400
);
}
wp_send_json_success(
array(
'message' => __( 'Your newsletter subscription has been confirmed. Thank you.', 'rx-theme' ),
)
);
}
/**
* Admin AJAX stats.
*/
public function ajax_stats() {
$this->verify_admin_ajax();
global $wpdb;
$table = $this->table_name();
$total = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table}" );
$active = (int) $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(*) FROM {$table} WHERE status = %s",
'active'
)
);
$pending = (int) $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(*) FROM {$table} WHERE status = %s",
'pending'
)
);
$unsubscribed = (int) $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(*) FROM {$table} WHERE status = %s",
'unsubscribed'
)
);
wp_send_json_success(
array(
'total' => $total,
'active' => $active,
'pending' => $pending,
'unsubscribed' => $unsubscribed,
)
);
}
/**
* Admin CSV export.
*/
public function ajax_export_csv() {
$this->verify_admin_ajax();
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'Permission denied.', 'rx-theme' ) );
}
global $wpdb;
$table = $this->table_name();
$rows = $wpdb->get_results(
"SELECT email, name, status, source, consent, confirmed_at, unsubscribed_at, created_at, updated_at FROM {$table} ORDER BY created_at DESC",
ARRAY_A
);
nocache_headers();
header( 'Content-Type: text/csv; charset=utf-8' );
header( 'Content-Disposition: attachment; filename=rx-newsletter-subscribers-' . gmdate( 'Y-m-d' ) . '.csv' );
$output = fopen( 'php://output', 'w' );
fputcsv(
$output,
array(
'Email',
'Name',
'Status',
'Source',
'Consent',
'Confirmed At',
'Unsubscribed At',
'Created At',
'Updated At',
)
);
foreach ( $rows as $row ) {
fputcsv(
$output,
array(
$row['email'],
$row['name'],
$row['status'],
$row['source'],
$row['consent'],
$row['confirmed_at'],
$row['unsubscribed_at'],
$row['created_at'],
$row['updated_at'],
)
);
}
fclose( $output );
exit;
}
/**
* Save subscriber.
*/
private function save_subscriber( $data ) {
global $wpdb;
$table = $this->table_name();
$email = isset( $data['email'] ) ? strtolower( sanitize_email( $data['email'] ) ) : '';
$name = isset( $data['name'] ) ? sanitize_text_field( $data['name'] ) : '';
if ( empty( $email ) || ! is_email( $email ) ) {
return new WP_Error( 'invalid_email', __( 'Invalid email address.', 'rx-theme' ) );
}
$existing = $this->get_subscriber_by_email( $email );
$now = current_time( 'mysql' );
$confirm_token = $this->generate_token();
$unsubscribe_token = $this->generate_token();
$record = array(
'email' => $email,
'name' => $name,
'status' => 'pending',
'source' => isset( $data['source'] ) ? sanitize_key( $data['source'] ) : 'unknown',
'consent' => ! empty( $data['consent'] ) ? 1 : 0,
'confirm_token' => $confirm_token,
'unsubscribe_token' => $unsubscribe_token,
'ip_hash' => $this->get_ip_hash(),
'user_agent' => $this->get_user_agent(),
'referrer' => $this->get_referrer(),
'meta' => wp_json_encode( $this->get_default_meta() ),
'updated_at' => $now,
);
if ( $existing ) {
if ( 'active' === $existing['status'] ) {
return new WP_Error(
'already_subscribed',
__( 'This email address is already subscribed.', 'rx-theme' )
);
}
$updated = $wpdb->update(
$table,
$record,
array( 'email' => $email ),
array(
'%s',
'%s',
'%s',
'%s',
'%d',
'%s',
'%s',
'%s',
'%s',
'%s',
'%s',
'%s',
),
array( '%s' )
);
if ( false === $updated ) {
return new WP_Error(
'db_update_failed',
__( 'Could not update your subscription. Please try again.', 'rx-theme' )
);
}
return $this->get_subscriber_by_email( $email );
}
$record['created_at'] = $now;
$inserted = $wpdb->insert(
$table,
$record,
array(
'%s',
'%s',
'%s',
'%s',
'%d',
'%s',
'%s',
'%s',
'%s',
'%s',
'%s',
'%s',
'%s',
)
);
if ( false === $inserted ) {
return new WP_Error(
'db_insert_failed',
__( 'Could not save your subscription. Please try again.', 'rx-theme' )
);
}
return $this->get_subscriber_by_email( $email );
}
/**
* Confirm subscriber.
*/
private function confirm_subscriber( $email, $token ) {
global $wpdb;
$table = $this->table_name();
$subscriber = $this->get_subscriber_by_email( $email );
if ( ! $subscriber ) {
return new WP_Error( 'not_found', __( 'Subscriber not found.', 'rx-theme' ) );
}
if ( 'active' === $subscriber['status'] ) {
return true;
}
if ( empty( $subscriber['confirm_token'] ) || ! hash_equals( $subscriber['confirm_token'], $token ) ) {
return new WP_Error( 'invalid_token', __( 'Invalid confirmation token.', 'rx-theme' ) );
}
$updated = $wpdb->update(
$table,
array(
'status' => 'active',
'confirm_token' => '',
'confirmed_at' => current_time( 'mysql' ),
'updated_at' => current_time( 'mysql' ),
),
array(
'email' => $email,
),
array(
'%s',
'%s',
'%s',
'%s',
),
array(
'%s',
)
);
if ( false === $updated ) {
return new WP_Error( 'confirm_failed', __( 'Could not confirm subscription.', 'rx-theme' ) );
}
do_action( 'rx_newsletter_after_confirm', $email );
return true;
}
/**
* Unsubscribe subscriber.
*/
private function unsubscribe( $email, $token = '' ) {
global $wpdb;
$table = $this->table_name();
$subscriber = $this->get_subscriber_by_email( $email );
if ( ! $subscriber ) {
return new WP_Error( 'not_found', __( 'Subscriber not found.', 'rx-theme' ) );
}
if ( ! empty( $subscriber['unsubscribe_token'] ) && ! empty( $token ) ) {
if ( ! hash_equals( $subscriber['unsubscribe_token'], $token ) ) {
return new WP_Error( 'invalid_token', __( 'Invalid unsubscribe token.', 'rx-theme' ) );
}
}
$updated = $wpdb->update(
$table,
array(
'status' => 'unsubscribed',
'unsubscribed_at' => current_time( 'mysql' ),
'updated_at' => current_time( 'mysql' ),
),
array(
'email' => $email,
),
array(
'%s',
'%s',
'%s',
),
array(
'%s',
)
);
if ( false === $updated ) {
return new WP_Error( 'unsubscribe_failed', __( 'Could not unsubscribe this email.', 'rx-theme' ) );
}
do_action( 'rx_newsletter_after_unsubscribe', $email );
return true;
}
/**
* Get subscriber by email.
*/
private function get_subscriber_by_email( $email ) {
global $wpdb;
$table = $this->table_name();
$row = $wpdb->get_row(
$wpdb->prepare(
"SELECT * FROM {$table} WHERE email = %s LIMIT 1",
strtolower( $email )
),
ARRAY_A
);
return $row;
}
/**
* Send confirmation email.
*/
private function send_confirm_email( $subscriber ) {
$email = $subscriber['email'];
$confirm_url = add_query_arg(
array(
'rx_newsletter_action' => 'confirm',
'email' => rawurlencode( $email ),
'token' => rawurlencode( $subscriber['confirm_token'] ),
),
home_url( '/' )
);
$unsubscribe_url = add_query_arg(
array(
'rx_newsletter_action' => 'unsubscribe',
'email' => rawurlencode( $email ),
'token' => rawurlencode( $subscriber['unsubscribe_token'] ),
),
home_url( '/' )
);
$site_name = wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES );
$subject = sprintf(
/* translators: %s site name */
__( 'Confirm your subscription to %s', 'rx-theme' ),
$site_name
);
$message = sprintf(
'<p>%s</p><p><a href="%s">%s</a></p><p>%s</p><p><a href="%s">%s</a></p>',
esc_html__( 'Thank you for subscribing. Please confirm your email address by clicking the button below.', 'rx-theme' ),
esc_url( $confirm_url ),
esc_html__( 'Confirm Subscription', 'rx-theme' ),
esc_html__( 'If you did not request this email, you may ignore it or unsubscribe here:', 'rx-theme' ),
esc_url( $unsubscribe_url ),
esc_html__( 'Unsubscribe', 'rx-theme' )
);
$headers = array(
'Content-Type: text/html; charset=UTF-8',
);
/**
* Filter confirmation email subject.
*/
$subject = apply_filters( 'rx_newsletter_confirm_email_subject', $subject, $subscriber );
/**
* Filter confirmation email message.
*/
$message = apply_filters( 'rx_newsletter_confirm_email_message', $message, $subscriber, $confirm_url, $unsubscribe_url );
return wp_mail( $email, $subject, $message, $headers );
}
/**
* Process confirm/unsubscribe query URL.
*
* Optional: call this on template_redirect from functions.php.
*/
public function handle_public_query_action() {
$action = isset( $_GET['rx_newsletter_action'] ) ? sanitize_key( wp_unslash( $_GET['rx_newsletter_action'] ) ) : '';
$email = isset( $_GET['email'] ) ? sanitize_email( wp_unslash( $_GET['email'] ) ) : '';
$token = isset( $_GET['token'] ) ? sanitize_text_field( wp_unslash( $_GET['token'] ) ) : '';
if ( empty( $action ) || empty( $email ) || empty( $token ) ) {
return;
}
if ( 'confirm' === $action ) {
$result = $this->confirm_subscriber( $email, $token );
$message = is_wp_error( $result )
? $result->get_error_message()
: __( 'Your subscription has been confirmed successfully.', 'rx-theme' );
wp_die(
esc_html( $message ),
esc_html__( 'Newsletter Confirmation', 'rx-theme' ),
array( 'response' => is_wp_error( $result ) ? 400 : 200 )
);
}
if ( 'unsubscribe' === $action ) {
$result = $this->unsubscribe( $email, $token );
$message = is_wp_error( $result )
? $result->get_error_message()
: __( 'You have been unsubscribed successfully.', 'rx-theme' );
wp_die(
esc_html( $message ),
esc_html__( 'Newsletter Unsubscribe', 'rx-theme' ),
array( 'response' => is_wp_error( $result ) ? 400 : 200 )
);
}
}
/**
* Verify public AJAX nonce.
*/
private function verify_nonce() {
$nonce = isset( $_POST[ self::NONCE_FIELD ] )
? sanitize_text_field( wp_unslash( $_POST[ self::NONCE_FIELD ] ) )
: '';
if ( ! wp_verify_nonce( $nonce, self::NONCE_ACTION ) ) {
wp_send_json_error(
array(
'message' => __( 'Security verification failed. Please refresh the page and try again.', 'rx-theme' ),
),
403
);
}
}
/**
* Verify admin AJAX request.
*/
private function verify_admin_ajax() {
$this->verify_nonce();
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error(
array(
'message' => __( 'Permission denied.', 'rx-theme' ),
),
403
);
}
}
/**
* Rate limit checker.
*/
private function is_rate_limited( $email ) {
$ip_hash = $this->get_ip_hash();
$email_hash = hash_hmac( 'sha256', strtolower( $email ), wp_salt( 'auth' ) );
$ip_key = self::RATE_PREFIX . $ip_hash;
$email_key = self::EMAIL_RATE_PREFIX . $email_hash;
$ip_count = (int) get_transient( $ip_key );
$email_count = (int) get_transient( $email_key );
$max_ip_attempts = (int) apply_filters( 'rx_newsletter_max_ip_attempts', 5 );
$max_email_attempts = (int) apply_filters( 'rx_newsletter_max_email_attempts', 3 );
$window = (int) apply_filters( 'rx_newsletter_rate_window', HOUR_IN_SECONDS );
if ( $ip_count >= $max_ip_attempts || $email_count >= $max_email_attempts ) {
return true;
}
set_transient( $ip_key, $ip_count + 1, $window );
set_transient( $email_key, $email_count + 1, $window );
return false;
}
/**
* Generate secure token.
*/
private function generate_token() {
return wp_hash( wp_generate_password( 64, true, true ) . microtime( true ) . wp_rand() );
}
/**
* Get privacy-safe IP hash.
*/
private function get_ip_hash() {
$ip = '';
if ( ! empty( $_SERVER['HTTP_CF_CONNECTING_IP'] ) ) {
$ip = sanitize_text_field( wp_unslash( $_SERVER['HTTP_CF_CONNECTING_IP'] ) );
} elseif ( ! empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) {
$forwarded = sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_FORWARDED_FOR'] ) );
$parts = explode( ',', $forwarded );
$ip = trim( $parts[0] );
} elseif ( ! empty( $_SERVER['REMOTE_ADDR'] ) ) {
$ip = sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) );
}
if ( empty( $ip ) ) {
$ip = 'unknown';
}
return hash_hmac( 'sha256', $ip, wp_salt( 'auth' ) );
}
/**
* Get user agent.
*/
private function get_user_agent() {
if ( empty( $_SERVER['HTTP_USER_AGENT'] ) ) {
return '';
}
return substr( sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ), 0, 500 );
}
/**
* Get referrer.
*/
private function get_referrer() {
if ( empty( $_SERVER['HTTP_REFERER'] ) ) {
return '';
}
return esc_url_raw( wp_unslash( $_SERVER['HTTP_REFERER'] ) );
}
/**
* Default subscriber metadata.
*/
private function get_default_meta() {
return array(
'user_id' => get_current_user_id(),
'language' => get_locale(),
'page_url' => $this->get_referrer(),
'subscribed' => current_time( 'mysql' ),
'site_url' => home_url( '/' ),
'site_name' => get_bloginfo( 'name' ),
);
}
/**
* Disposable email checker.
*/
private function is_disposable_email( $email ) {
$domain = substr( strrchr( strtolower( $email ), '@' ), 1 );
$blocked_domains = apply_filters(
'rx_newsletter_blocked_email_domains',
array(
'10minutemail.com',
'tempmail.com',
'mailinator.com',
'guerrillamail.com',
'yopmail.com',
'trashmail.com',
)
);
return in_array( $domain, $blocked_domains, true );
}
/**
* Log security event.
*/
private function log_security_event( $event ) {
if ( ! defined( 'WP_DEBUG' ) || ! WP_DEBUG ) {
return;
}
error_log(
sprintf(
'RX Newsletter security event: %s | IP hash: %s',
sanitize_key( $event ),
$this->get_ip_hash()
)
);
}
}
endif;
/**
* Start RX Newsletter AJAX.
*/
function rx_theme_newsletter_ajax() {
return RX_Theme_Newsletter_Ajax::instance();
}
rx_theme_newsletter_ajax();
/**
* Handle public email confirmation and unsubscribe URLs.
*/
add_action(
'template_redirect',
function () {
rx_theme_newsletter_ajax()->handle_public_query_action();
}
);
Now include it in your theme functions.php:
/**
* RX Theme Newsletter AJAX.
*/
$rx_newsletter_ajax_file = get_template_directory() . '/inc/ajax/newsletter.php';
if ( file_exists( $rx_newsletter_ajax_file ) ) {
require_once $rx_newsletter_ajax_file;
}
For frontend JavaScript, your AJAX request should send data to:
admin_url( 'admin-ajax.php' )
Example action name:
action: "rx_newsletter_subscribe"
And the form can be shown anywhere with this shortcode:
[rx_newsletter_form]
This file is very advanced for a theme-level newsletter system, but for large real-world email campaigns, it is still better to connect it later with Mailchimp, Brevo, FluentCRM, MailPoet, ConvertKit, or your own email API, because normal WordPress wp_mail() is not ideal for bulk sending.