Files
WooNooW/includes/Api/NewsletterController.php

554 lines
20 KiB
PHP

<?php
namespace WooNooW\API;
use WP_REST_Request;
use WP_REST_Response;
use WP_Error;
use WooNooW\Core\Validation;
use WooNooW\Database\SubscriberTable;
class NewsletterController
{
const API_NAMESPACE = 'woonoow/v1';
public static function register_routes()
{
register_rest_route(self::API_NAMESPACE, '/newsletter/subscribe', [
'methods' => 'POST',
'callback' => [__CLASS__, 'subscribe'],
'permission_callback' => '__return_true',
'args' => [
'email' => [
'required' => true,
'type' => 'string',
'validate_callback' => function ($param) {
return is_email($param);
},
],
],
]);
register_rest_route(self::API_NAMESPACE, '/newsletter/subscribers', [
'methods' => 'GET',
'callback' => [__CLASS__, 'get_subscribers'],
'permission_callback' => function () {
return current_user_can('manage_options');
},
]);
register_rest_route(self::API_NAMESPACE, '/newsletter/subscribers/(?P<email>[^/]+)', [
'methods' => 'DELETE',
'callback' => [__CLASS__, 'delete_subscriber'],
'permission_callback' => function () {
return current_user_can('manage_options');
},
]);
register_rest_route(self::API_NAMESPACE, '/newsletter/template/(?P<template>[^/]+)', [
'methods' => 'GET',
'callback' => [__CLASS__, 'get_template'],
'permission_callback' => function () {
return current_user_can('manage_options');
},
]);
register_rest_route(self::API_NAMESPACE, '/newsletter/template/(?P<template>[^/]+)', [
'methods' => 'POST',
'callback' => [__CLASS__, 'save_template'],
'permission_callback' => function () {
return current_user_can('manage_options');
},
]);
// Public unsubscribe endpoint (no auth needed, uses token)
register_rest_route(self::API_NAMESPACE, '/newsletter/unsubscribe', [
'methods' => 'GET',
'callback' => [__CLASS__, 'unsubscribe'],
'permission_callback' => '__return_true',
'args' => [
'email' => [
'required' => true,
'type' => 'string',
],
'token' => [
'required' => true,
'type' => 'string',
],
],
]);
// Public confirm endpoint (double opt-in)
register_rest_route(self::API_NAMESPACE, '/newsletter/confirm', [
'methods' => 'GET',
'callback' => [__CLASS__, 'confirm'],
'permission_callback' => '__return_true',
'args' => [
'email' => [
'required' => true,
'type' => 'string',
],
'token' => [
'required' => true,
'type' => 'string',
],
],
]);
}
public static function get_template(WP_REST_Request $request)
{
$template = $request->get_param('template');
$option_key = "woonoow_newsletter_{$template}_template";
$data = get_option($option_key, [
'subject' => $template === 'welcome' ? 'Welcome to {site_name} Newsletter!' : 'Confirm your newsletter subscription',
'content' => $template === 'welcome'
? "Thank you for subscribing to our newsletter!\n\nYou'll receive updates about our latest products and offers.\n\nBest regards,\n{site_name}"
: "Please confirm your newsletter subscription by clicking the link below:\n\n{confirmation_url}\n\nBest regards,\n{site_name}",
]);
return new WP_REST_Response([
'success' => true,
'subject' => $data['subject'] ?? '',
'content' => $data['content'] ?? '',
], 200);
}
public static function save_template(WP_REST_Request $request)
{
$template = $request->get_param('template');
$subject = sanitize_text_field($request->get_param('subject'));
$content = wp_kses_post($request->get_param('content'));
$option_key = "woonoow_newsletter_{$template}_template";
update_option($option_key, [
'subject' => $subject,
'content' => $content,
]);
return new WP_REST_Response([
'success' => true,
'message' => 'Template saved successfully',
], 200);
}
public static function delete_subscriber(WP_REST_Request $request)
{
$email = sanitize_email(urldecode($request->get_param('email')));
if (self::use_custom_table()) {
$result = SubscriberTable::delete_by_email($email);
if ($result) {
return new WP_REST_Response([
'success' => true,
'message' => 'Subscriber removed successfully',
], 200);
}
return new WP_Error('not_found', 'Subscriber not found', ['status' => 404]);
}
// Legacy: wp_options storage
$subscribers = get_option('woonoow_newsletter_subscribers', []);
$subscribers = array_filter($subscribers, function ($sub) use ($email) {
return isset($sub['email']) && $sub['email'] !== $email;
});
update_option('woonoow_newsletter_subscribers', array_values($subscribers));
return new WP_REST_Response([
'success' => true,
'message' => 'Subscriber removed successfully',
], 200);
}
/**
* Check if custom subscriber table should be used
*/
private static function use_custom_table()
{
return SubscriberTable::table_exists();
}
public static function subscribe(WP_REST_Request $request)
{
$email = sanitize_email($request->get_param('email'));
$consent = (bool) $request->get_param('consent');
// Rate limiting (5 requests per IP per hour)
$ip = $_SERVER['REMOTE_ADDR'] ?? '';
$rate_key = 'woonoow_newsletter_rate_' . md5($ip);
$attempts = (int) get_transient($rate_key);
if ($attempts >= 5) {
return new WP_Error('rate_limited', __('Too many requests. Please try again later.', 'woonoow'), ['status' => 429]);
}
set_transient($rate_key, $attempts + 1, HOUR_IN_SECONDS);
// Use centralized validation with extensible filter hooks
$validation = Validation::validate_email($email, 'newsletter_subscribe');
if (is_wp_error($validation)) {
return $validation;
}
// Check GDPR consent requirement
$gdpr_required = get_option('woonoow_newsletter_gdpr_consent', false);
if ($gdpr_required && !$consent) {
return new WP_Error('consent_required', __('Please accept the terms to subscribe.', 'woonoow'), ['status' => 400]);
}
// Check if email belongs to a WP user
$user = get_user_by('email', $email);
$user_id = $user ? $user->ID : null;
// Check double opt-in setting
$double_opt_in = get_option('woonoow_newsletter_double_opt_in', true);
$status = $double_opt_in ? 'pending' : 'active';
if (self::use_custom_table()) {
// Use custom table
$existing = SubscriberTable::get_by_email($email);
if ($existing) {
if ($existing['status'] === 'active') {
return new WP_REST_Response([
'success' => true,
'message' => __('You are already subscribed to our newsletter!', 'woonoow'),
], 200);
}
if ($existing['status'] === 'pending') {
self::send_confirmation_email($email, $existing['user_id'] ?? null);
return new WP_REST_Response([
'success' => true,
'message' => __('Confirmation email resent. Please check your inbox.', 'woonoow'),
], 200);
}
// Resubscribe (was unsubscribed)
SubscriberTable::update_by_email($email, [
'status' => $status,
'consent' => $consent ? 1 : 0,
'subscribed_at' => current_time('mysql'),
'ip_address' => $ip,
]);
} else {
// New subscriber
SubscriberTable::add([
'email' => $email,
'user_id' => $user_id,
'status' => $status,
'consent' => $consent,
'subscribed_at' => current_time('mysql'),
'ip_address' => $ip,
]);
}
} else {
// Legacy: wp_options storage
$subscribers = get_option('woonoow_newsletter_subscribers', []);
// Check if already subscribed
$existing_key = null;
foreach ($subscribers as $key => $sub) {
if (isset($sub['email']) && $sub['email'] === $email) {
$existing_key = $key;
break;
}
}
if ($existing_key !== null) {
$existing = $subscribers[$existing_key];
if (($existing['status'] ?? 'active') === 'active') {
return new WP_REST_Response([
'success' => true,
'message' => __('You are already subscribed to our newsletter!', 'woonoow'),
], 200);
}
if (($existing['status'] ?? '') === 'pending') {
self::send_confirmation_email($email, $existing['user_id'] ?? null);
return new WP_REST_Response([
'success' => true,
'message' => __('Confirmation email resent. Please check your inbox.', 'woonoow'),
], 200);
}
}
$subscribers[] = [
'email' => $email,
'user_id' => $user_id,
'status' => $status,
'consent' => $consent,
'subscribed_at' => current_time('mysql'),
'ip_address' => $ip,
];
update_option('woonoow_newsletter_subscribers', $subscribers);
}
if ($double_opt_in) {
// Send confirmation email
self::send_confirmation_email($email, $user_id);
return new WP_REST_Response([
'success' => true,
'message' => __('Please check your email to confirm your subscription.', 'woonoow'),
], 200);
}
// Direct subscription (no double opt-in)
do_action('woonoow_newsletter_subscribed', $email, $user_id);
do_action('woonoow/notification/event', 'newsletter_welcome', 'customer', [
'email' => $email,
'user_id' => $user_id,
'subscribed_at' => current_time('mysql'),
]);
do_action('woonoow/notification/event', 'newsletter_subscribed_admin', 'staff', [
'email' => $email,
'user_id' => $user_id,
'subscribed_at' => current_time('mysql'),
]);
return new WP_REST_Response([
'success' => true,
'message' => __('Successfully subscribed to our newsletter!', 'woonoow'),
], 200);
}
/**
* Send confirmation email for double opt-in
*/
private static function send_confirmation_email($email, $user_id = null)
{
$confirmation_url = self::generate_confirmation_url($email);
do_action('woonoow/notification/event', 'newsletter_confirm', 'customer', [
'email' => $email,
'user_id' => $user_id,
'confirmation_url' => $confirmation_url,
]);
}
/**
* Generate confirmation URL with secure token
*/
public static function generate_confirmation_url($email)
{
$token = self::generate_unsubscribe_token($email); // Reuse same token logic
$base_url = rest_url('woonoow/v1/newsletter/confirm');
return add_query_arg([
'email' => urlencode($email),
'token' => $token,
], $base_url);
}
/**
* Handle confirmation request (double opt-in)
*/
public static function confirm(WP_REST_Request $request)
{
$email = sanitize_email(urldecode($request->get_param('email')));
$token = sanitize_text_field($request->get_param('token'));
// Verify token
$expected_token = self::generate_unsubscribe_token($email);
if (!hash_equals($expected_token, $token)) {
return new WP_REST_Response([
'success' => false,
'message' => __('Invalid confirmation link', 'woonoow'),
], 400);
}
$found = false;
$user_id = null;
if (self::use_custom_table()) {
$existing = SubscriberTable::get_by_email($email);
if ($existing) {
if ($existing['status'] === 'active') {
$found = true;
} else {
SubscriberTable::update_by_email($email, [
'status' => 'active',
'confirmed_at' => current_time('mysql'),
]);
$user_id = $existing['user_id'] ?? null;
$found = true;
}
}
} else {
// Legacy: wp_options
$subscribers = get_option('woonoow_newsletter_subscribers', []);
foreach ($subscribers as &$sub) {
if (isset($sub['email']) && $sub['email'] === $email) {
if (($sub['status'] ?? '') === 'active') {
$found = true;
break;
}
$sub['status'] = 'active';
$sub['confirmed_at'] = current_time('mysql');
$user_id = $sub['user_id'] ?? null;
$found = true;
break;
}
}
if ($found) {
update_option('woonoow_newsletter_subscribers', $subscribers);
}
}
// Trigger subscription events
do_action('woonoow_newsletter_subscribed', $email, $user_id);
do_action('woonoow/notification/event', 'newsletter_welcome', 'customer', [
'email' => $email,
'user_id' => $user_id,
'subscribed_at' => current_time('mysql'),
]);
do_action('woonoow/notification/event', 'newsletter_subscribed_admin', 'staff', [
'email' => $email,
'user_id' => $user_id,
'subscribed_at' => current_time('mysql'),
]);
// Return HTML page for nice UX
$site_name = get_bloginfo('name');
$shop_url = wc_get_page_permalink('shop') ?: home_url();
$html = sprintf(
'<!DOCTYPE html><html><head><title>%s</title><style>body{font-family:system-ui,sans-serif;display:flex;justify-content:center;align-items:center;min-height:100vh;margin:0;background:#f5f5f5;}.box{background:white;padding:40px;border-radius:8px;text-align:center;box-shadow:0 2px 10px rgba(0,0,0,0.1);max-width:400px;}h1{color:#333;margin-bottom:16px;}p{color:#666;}a{display:inline-block;margin-top:20px;padding:12px 24px;background:#333;color:white;text-decoration:none;border-radius:6px;}</style></head><body><div class="box"><h1>✓ Confirmed!</h1><p>You are now subscribed to %s newsletter.</p><a href="%s">Continue Shopping</a></div></body></html>',
__('Subscription Confirmed', 'woonoow'),
esc_html($site_name),
esc_url($shop_url)
);
header('Content-Type: text/html; charset=utf-8');
echo $html;
exit;
}
// Dead code removed: send_welcome_email() - now handled via notification system
public static function get_subscribers(WP_REST_Request $request)
{
if (self::use_custom_table()) {
$result = SubscriberTable::get_all([
'per_page' => 100,
'page' => 1,
]);
return new WP_REST_Response([
'success' => true,
'data' => [
'subscribers' => $result['items'],
'count' => $result['total'],
],
], 200);
}
// Legacy: wp_options
$subscribers = get_option('woonoow_newsletter_subscribers', []);
return new WP_REST_Response([
'success' => true,
'data' => [
'subscribers' => $subscribers,
'count' => count($subscribers),
],
], 200);
}
/**
* Handle unsubscribe request
*/
public static function unsubscribe(WP_REST_Request $request)
{
$email = sanitize_email(urldecode($request->get_param('email')));
$token = sanitize_text_field($request->get_param('token'));
// Verify token
$expected_token = self::generate_unsubscribe_token($email);
if (!hash_equals($expected_token, $token)) {
return new WP_REST_Response([
'success' => false,
'message' => __('Invalid unsubscribe link', 'woonoow'),
], 400);
}
$found = false;
if (self::use_custom_table()) {
$existing = SubscriberTable::get_by_email($email);
if ($existing) {
SubscriberTable::update_by_email($email, [
'status' => 'unsubscribed',
'unsubscribed_at' => current_time('mysql'),
]);
$found = true;
}
} else {
// Legacy: wp_options
$subscribers = get_option('woonoow_newsletter_subscribers', []);
foreach ($subscribers as &$sub) {
if (isset($sub['email']) && $sub['email'] === $email) {
$sub['status'] = 'unsubscribed';
$sub['unsubscribed_at'] = current_time('mysql');
$found = true;
break;
}
}
if ($found) {
update_option('woonoow_newsletter_subscribers', $subscribers);
}
}
if (!$found) {
return new WP_REST_Response([
'success' => false,
'message' => __('Email not found', 'woonoow'),
], 404);
}
do_action('woonoow_newsletter_unsubscribed', $email);
// Return HTML page for nice UX
$site_name = get_bloginfo('name');
$html = sprintf(
'<!DOCTYPE html><html><head><title>%s</title><style>body{font-family:system-ui,sans-serif;display:flex;justify-content:center;align-items:center;min-height:100vh;margin:0;background:#f5f5f5;}.box{background:white;padding:40px;border-radius:8px;text-align:center;box-shadow:0 2px 10px rgba(0,0,0,0.1);max-width:400px;}h1{color:#333;margin-bottom:16px;}p{color:#666;}</style></head><body><div class="box"><h1>✓ Unsubscribed</h1><p>You have been unsubscribed from %s newsletter.</p></div></body></html>',
__('Unsubscribed', 'woonoow'),
esc_html($site_name)
);
header('Content-Type: text/html; charset=utf-8');
echo $html;
exit;
}
/**
* Generate secure unsubscribe token
*/
private static function generate_unsubscribe_token($email)
{
$secret = wp_salt('auth');
return hash_hmac('sha256', $email, $secret);
}
/**
* Generate unsubscribe URL for email templates
*/
public static function generate_unsubscribe_url($email)
{
$token = self::generate_unsubscribe_token($email);
$base_url = rest_url('woonoow/v1/newsletter/unsubscribe');
return add_query_arg([
'email' => urlencode($email),
'token' => $token,
], $base_url);
}
}