fix: resolve container width issues, spa redirects, and appearance settings overwrite. feat: enhance order/sub details and newsletter layout

This commit is contained in:
Dwindi Ramadhana
2026-02-05 00:09:40 +07:00
parent a0b5f8496d
commit 5f08c18ec7
77 changed files with 7027 additions and 4546 deletions

View File

@@ -1,4 +1,5 @@
<?php
/**
* Campaigns REST Controller
*
@@ -13,64 +14,67 @@ use WP_REST_Request;
use WP_REST_Response;
use WP_Error;
use WooNooW\Core\Campaigns\CampaignManager;
use WooNooW\Core\ActivityLog\Logger;
class CampaignsController
{
class CampaignsController {
const API_NAMESPACE = 'woonoow/v1';
/**
* Register REST routes
*/
public static function register_routes() {
public static function register_routes()
{
// List campaigns
register_rest_route(self::API_NAMESPACE, '/campaigns', [
'methods' => 'GET',
'callback' => [__CLASS__, 'get_campaigns'],
'permission_callback' => [__CLASS__, 'check_admin_permission'],
]);
// Create campaign
register_rest_route(self::API_NAMESPACE, '/campaigns', [
'methods' => 'POST',
'callback' => [__CLASS__, 'create_campaign'],
'permission_callback' => [__CLASS__, 'check_admin_permission'],
]);
// Get single campaign
register_rest_route(self::API_NAMESPACE, '/campaigns/(?P<id>\d+)', [
'methods' => 'GET',
'callback' => [__CLASS__, 'get_campaign'],
'permission_callback' => [__CLASS__, 'check_admin_permission'],
]);
// Update campaign
register_rest_route(self::API_NAMESPACE, '/campaigns/(?P<id>\d+)', [
'methods' => 'PUT',
'callback' => [__CLASS__, 'update_campaign'],
'permission_callback' => [__CLASS__, 'check_admin_permission'],
]);
// Delete campaign
register_rest_route(self::API_NAMESPACE, '/campaigns/(?P<id>\d+)', [
'methods' => 'DELETE',
'callback' => [__CLASS__, 'delete_campaign'],
'permission_callback' => [__CLASS__, 'check_admin_permission'],
]);
// Send campaign
register_rest_route(self::API_NAMESPACE, '/campaigns/(?P<id>\d+)/send', [
'methods' => 'POST',
'callback' => [__CLASS__, 'send_campaign'],
'permission_callback' => [__CLASS__, 'check_admin_permission'],
]);
// Send test email
register_rest_route(self::API_NAMESPACE, '/campaigns/(?P<id>\d+)/test', [
'methods' => 'POST',
'callback' => [__CLASS__, 'send_test_email'],
'permission_callback' => [__CLASS__, 'check_admin_permission'],
]);
// Preview campaign
register_rest_route(self::API_NAMESPACE, '/campaigns/(?P<id>\d+)/preview', [
'methods' => 'GET',
@@ -78,30 +82,33 @@ class CampaignsController {
'permission_callback' => [__CLASS__, 'check_admin_permission'],
]);
}
/**
* Check admin permission
*/
public static function check_admin_permission() {
public static function check_admin_permission()
{
return current_user_can('manage_options');
}
/**
* Get all campaigns
*/
public static function get_campaigns(WP_REST_Request $request) {
public static function get_campaigns(WP_REST_Request $request)
{
$campaigns = CampaignManager::get_all();
return new WP_REST_Response([
'success' => true,
'data' => $campaigns,
]);
}
/**
* Create campaign
*/
public static function create_campaign(WP_REST_Request $request) {
public static function create_campaign(WP_REST_Request $request)
{
$data = [
'title' => $request->get_param('title'),
'subject' => $request->get_param('subject'),
@@ -109,52 +116,54 @@ class CampaignsController {
'status' => $request->get_param('status') ?: 'draft',
'scheduled_at' => $request->get_param('scheduled_at'),
];
$campaign_id = CampaignManager::create($data);
if (is_wp_error($campaign_id)) {
return new WP_REST_Response([
'success' => false,
'error' => $campaign_id->get_error_message(),
], 400);
}
$campaign = CampaignManager::get($campaign_id);
return new WP_REST_Response([
'success' => true,
'data' => $campaign,
], 201);
}
/**
* Get single campaign
*/
public static function get_campaign(WP_REST_Request $request) {
public static function get_campaign(WP_REST_Request $request)
{
$campaign_id = (int) $request->get_param('id');
$campaign = CampaignManager::get($campaign_id);
if (!$campaign) {
return new WP_REST_Response([
'success' => false,
'error' => __('Campaign not found', 'woonoow'),
], 404);
}
return new WP_REST_Response([
'success' => true,
'data' => $campaign,
]);
}
/**
* Update campaign
*/
public static function update_campaign(WP_REST_Request $request) {
public static function update_campaign(WP_REST_Request $request)
{
$campaign_id = (int) $request->get_param('id');
$data = [];
if ($request->has_param('title')) {
$data['title'] = $request->get_param('title');
}
@@ -170,60 +179,62 @@ class CampaignsController {
if ($request->has_param('scheduled_at')) {
$data['scheduled_at'] = $request->get_param('scheduled_at');
}
$result = CampaignManager::update($campaign_id, $data);
if (is_wp_error($result)) {
return new WP_REST_Response([
'success' => false,
'error' => $result->get_error_message(),
], 400);
}
$campaign = CampaignManager::get($campaign_id);
return new WP_REST_Response([
'success' => true,
'data' => $campaign,
]);
}
/**
* Delete campaign
*/
public static function delete_campaign(WP_REST_Request $request) {
public static function delete_campaign(WP_REST_Request $request)
{
$campaign_id = (int) $request->get_param('id');
$result = CampaignManager::delete($campaign_id);
if (!$result) {
return new WP_REST_Response([
'success' => false,
'error' => __('Failed to delete campaign', 'woonoow'),
], 400);
}
return new WP_REST_Response([
'success' => true,
'message' => __('Campaign deleted', 'woonoow'),
]);
}
/**
* Send campaign
*/
public static function send_campaign(WP_REST_Request $request) {
public static function send_campaign(WP_REST_Request $request)
{
$campaign_id = (int) $request->get_param('id');
$result = CampaignManager::send($campaign_id);
if (!$result['success']) {
return new WP_REST_Response([
'success' => false,
'error' => $result['error'],
], 400);
}
return new WP_REST_Response([
'success' => true,
'message' => sprintf(
@@ -236,63 +247,80 @@ class CampaignsController {
'total' => $result['total'],
]);
}
/**
* Send test email
*/
public static function send_test_email(WP_REST_Request $request) {
public static function send_test_email(WP_REST_Request $request)
{
$campaign_id = (int) $request->get_param('id');
$email = sanitize_email($request->get_param('email'));
if (!is_email($email)) {
return new WP_REST_Response([
'success' => false,
'error' => __('Invalid email address', 'woonoow'),
], 400);
}
$result = CampaignManager::send_test($campaign_id, $email);
if (!$result) {
return new WP_REST_Response([
'success' => false,
'error' => __('Failed to send test email', 'woonoow'),
], 400);
}
// Log to activity log
Logger::log(
'test_sent',
'campaign',
$campaign_id,
sprintf(__('Test email sent to %s', 'woonoow'), $email)
);
return new WP_REST_Response([
'success' => true,
'message' => sprintf(__('Test email sent to %s', 'woonoow'), $email),
]);
}
/**
* Preview campaign
*/
public static function preview_campaign(WP_REST_Request $request) {
public static function preview_campaign(WP_REST_Request $request)
{
$campaign_id = (int) $request->get_param('id');
$campaign = CampaignManager::get($campaign_id);
if (!$campaign) {
return new WP_REST_Response([
'success' => false,
'error' => __('Campaign not found', 'woonoow'),
], 404);
}
// Use reflection to call private render method or make it public
// For now, return a simple preview
$renderer = \WooNooW\Core\Notifications\EmailRenderer::instance();
$template = $renderer->get_template_settings('newsletter_campaign', 'customer');
$content = $campaign['content'];
$subject = $campaign['subject'] ?: $campaign['title'];
if ($template) {
// Use template subject if available
if (!empty($template['subject'])) {
$subject = $template['subject'];
}
$content = str_replace('{content}', $campaign['content'], $template['body']);
$content = str_replace('{campaign_title}', $campaign['title'], $content);
}
// Replace campaign_title in subject
$subject = str_replace('{campaign_title}', $campaign['title'], $subject);
// Replace placeholders
$site_name = get_bloginfo('name');
$content = str_replace(['{site_name}', '{store_name}'], $site_name, $content);
@@ -301,7 +329,10 @@ class CampaignsController {
$content = str_replace('{unsubscribe_url}', '#unsubscribe', $content);
$content = str_replace('{current_date}', date_i18n(get_option('date_format')), $content);
$content = str_replace('{current_year}', date('Y'), $content);
// Parse card shortcodes before rendering
$content = $renderer->parse_cards($content);
// Render with design template
$design_path = $renderer->get_design_template();
if (file_exists($design_path)) {
@@ -310,7 +341,7 @@ class CampaignsController {
'site_url' => home_url(),
]);
}
return new WP_REST_Response([
'success' => true,
'subject' => $subject,

View File

@@ -311,12 +311,27 @@ class CheckoutController
return ['error' => __('No items provided', 'woonoow')];
}
// Security: Rate limiting check
if (\WooNooW\Compat\SecuritySettingsProvider::is_rate_limited()) {
return ['error' => __('Too many orders. Please try again later.', 'woonoow')];
}
// Security: CAPTCHA validation
$captcha_token = $payload['captcha_token'] ?? '';
$captcha_result = \WooNooW\Compat\SecuritySettingsProvider::validate_captcha($captcha_token);
if (is_wp_error($captcha_result)) {
return ['error' => $captcha_result->get_error_message()];
}
// Create order
$order = wc_create_order();
if (is_wp_error($order)) {
return ['error' => $order->get_error_message()];
}
// Track if user was logged in during this request (for frontend page reload)
$user_logged_in = false;
// Set customer ID if user is logged in
if (is_user_logged_in()) {
$user_id = get_current_user_id();
@@ -358,8 +373,9 @@ class CheckoutController
$existing_user = get_user_by('email', $email);
if ($existing_user) {
// User exists - link order to them
// User exists - link order to them (but do NOT auto-login for security)
$order->set_customer_id($existing_user->ID);
// Note: user_logged_in stays false - existing users must authenticate separately
} else {
// Create new user account
$password = wp_generate_password(12, true, true);
@@ -387,6 +403,7 @@ class CheckoutController
// AUTO-LOGIN: Set authentication cookie so user is logged in after page reload
wp_set_auth_cookie($new_user_id, true);
wp_set_current_user($new_user_id);
$user_logged_in = true;
// Set WooCommerce customer billing data
$customer = new \WC_Customer($new_user_id);
@@ -509,6 +526,9 @@ class CheckoutController
WC()->cart->empty_cart();
}
// Record this order attempt for rate limiting
\WooNooW\Compat\SecuritySettingsProvider::record_order_attempt();
return [
'ok' => true,
'order_id' => $order->get_id(),
@@ -516,6 +536,7 @@ class CheckoutController
'status' => $order->get_status(),
'pay_url' => $order->get_checkout_payment_url(),
'thankyou_url' => $order->get_checkout_order_received_url(),
'user_logged_in' => $user_logged_in, // True if user was logged in during this request (requires page reload)
];
}

View File

@@ -1,15 +1,19 @@
<?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 {
class NewsletterController
{
const API_NAMESPACE = 'woonoow/v1';
public static function register_routes() {
public static function register_routes()
{
register_rest_route(self::API_NAMESPACE, '/newsletter/subscribe', [
'methods' => 'POST',
'callback' => [__CLASS__, 'subscribe'],
@@ -18,45 +22,45 @@ class NewsletterController {
'email' => [
'required' => true,
'type' => 'string',
'validate_callback' => function($param) {
'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() {
'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() {
'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() {
'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() {
'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',
@@ -73,139 +77,381 @@ class NewsletterController {
],
],
]);
// 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) {
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'
'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) {
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 = urldecode($request->get_param('email'));
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) {
$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);
}
public static function subscribe(WP_REST_Request $request) {
/**
* 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;
}
// Get existing subscribers (now stored as objects with metadata)
$subscribers = get_option('woonoow_newsletter_subscribers', []);
// Check if already subscribed
$existing = array_filter($subscribers, function($sub) use ($email) {
return isset($sub['email']) && $sub['email'] === $email;
});
if (!empty($existing)) {
return new WP_REST_Response([
'success' => true,
'message' => 'You are already subscribed to our newsletter!',
], 200);
// 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;
// Add new subscriber with metadata
$subscribers[] = [
'email' => $email,
'user_id' => $user_id,
'status' => 'active',
'subscribed_at' => current_time('mysql'),
'ip_address' => $_SERVER['REMOTE_ADDR'] ?? '',
];
update_option('woonoow_newsletter_subscribers', $subscribers);
// Trigger notification events
// 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);
// Trigger notification system events (uses email builder)
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! Check your email for confirmation.',
'message' => __('Successfully subscribed to our newsletter!', 'woonoow'),
], 200);
}
private static function send_welcome_email($email) {
$site_name = get_bloginfo('name');
$template = get_option('woonoow_newsletter_welcome_template', '');
if (empty($template)) {
$template = "Thank you for subscribing to our newsletter!\n\nYou'll receive updates about our latest products and offers.\n\nBest regards,\n{site_name}";
}
$subject = sprintf('Welcome to %s Newsletter!', $site_name);
$message = str_replace('{site_name}', $site_name, $template);
wp_mail($email, $subject, $message);
/**
* 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,
]);
}
public static function get_subscribers(WP_REST_Request $request) {
/**
* 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' => [
@@ -214,14 +460,15 @@ class NewsletterController {
],
], 200);
}
/**
* Handle unsubscribe request
*/
public static function unsubscribe(WP_REST_Request $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)) {
@@ -230,31 +477,45 @@ class NewsletterController {
'message' => __('Invalid unsubscribe link', 'woonoow'),
], 400);
}
// Get subscribers
$subscribers = get_option('woonoow_newsletter_subscribers', []);
$found = false;
foreach ($subscribers as &$sub) {
if (isset($sub['email']) && $sub['email'] === $email) {
$sub['status'] = 'unsubscribed';
$sub['unsubscribed_at'] = current_time('mysql');
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;
break;
}
} 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);
}
update_option('woonoow_newsletter_subscribers', $subscribers);
do_action('woonoow_newsletter_unsubscribed', $email);
// Return HTML page for nice UX
$site_name = get_bloginfo('name');
$html = sprintf(
@@ -262,24 +523,26 @@ class NewsletterController {
__('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) {
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) {
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([
@@ -288,4 +551,3 @@ class NewsletterController {
], $base_url);
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -371,13 +371,13 @@ class ProductsController
}
// Virtual and downloadable
if (isset($data['virtual'])) {
if (array_key_exists('virtual', $data)) {
$product->set_virtual((bool) $data['virtual']);
}
if (isset($data['downloadable'])) {
if (array_key_exists('downloadable', $data)) {
$product->set_downloadable((bool) $data['downloadable']);
}
if (isset($data['featured'])) {
if (array_key_exists('featured', $data)) {
$product->set_featured((bool) $data['featured']);
}
@@ -510,13 +510,13 @@ class ProductsController
if (isset($data['height'])) $product->set_height(self::sanitize_number($data['height']));
// Virtual and downloadable
if (isset($data['virtual'])) {
if (array_key_exists('virtual', $data)) {
$product->set_virtual((bool) $data['virtual']);
}
if (isset($data['downloadable'])) {
if (array_key_exists('downloadable', $data)) {
$product->set_downloadable((bool) $data['downloadable']);
}
if (isset($data['featured'])) {
if (array_key_exists('featured', $data)) {
$product->set_featured((bool) $data['featured']);
}

View File

@@ -1,4 +1,5 @@
<?php
/**
* Store REST API Controller
*
@@ -11,28 +12,31 @@ namespace WooNooW\API;
use WooNooW\Compat\StoreSettingsProvider;
use WooNooW\Compat\CustomerSettingsProvider;
use WooNooW\Compat\SecuritySettingsProvider;
use WP_REST_Controller;
use WP_REST_Server;
use WP_REST_Request;
use WP_REST_Response;
use WP_Error;
class StoreController extends WP_REST_Controller {
class StoreController extends WP_REST_Controller
{
/**
* Namespace
*/
protected $namespace = 'woonoow/v1';
/**
* Rest base
*/
protected $rest_base = 'store';
/**
* Register routes
*/
public function register_routes() {
public function register_routes()
{
// GET /woonoow/v1/store/branding (PUBLIC - for login page)
register_rest_route($this->namespace, '/' . $this->rest_base . '/branding', [
[
@@ -41,7 +45,7 @@ class StoreController extends WP_REST_Controller {
'permission_callback' => '__return_true', // Public endpoint
],
]);
// GET /woonoow/v1/store/settings
register_rest_route($this->namespace, '/' . $this->rest_base . '/settings', [
[
@@ -50,7 +54,7 @@ class StoreController extends WP_REST_Controller {
'permission_callback' => [$this, 'check_permission'],
],
]);
// POST /woonoow/v1/store/settings
register_rest_route($this->namespace, '/' . $this->rest_base . '/settings', [
[
@@ -59,7 +63,7 @@ class StoreController extends WP_REST_Controller {
'permission_callback' => [$this, 'check_permission'],
],
]);
// GET /woonoow/v1/store/countries
register_rest_route($this->namespace, '/' . $this->rest_base . '/countries', [
[
@@ -68,7 +72,7 @@ class StoreController extends WP_REST_Controller {
'permission_callback' => [$this, 'check_permission'],
],
]);
// GET /woonoow/v1/store/timezones
register_rest_route($this->namespace, '/' . $this->rest_base . '/timezones', [
[
@@ -77,7 +81,7 @@ class StoreController extends WP_REST_Controller {
'permission_callback' => [$this, 'check_permission'],
],
]);
// GET /woonoow/v1/store/currencies
register_rest_route($this->namespace, '/' . $this->rest_base . '/currencies', [
[
@@ -86,7 +90,7 @@ class StoreController extends WP_REST_Controller {
'permission_callback' => [$this, 'check_permission'],
],
]);
// GET /woonoow/v1/store/customer-settings
register_rest_route($this->namespace, '/' . $this->rest_base . '/customer-settings', [
[
@@ -95,7 +99,7 @@ class StoreController extends WP_REST_Controller {
'permission_callback' => [$this, 'check_permission'],
],
]);
// POST /woonoow/v1/store/customer-settings
register_rest_route($this->namespace, '/' . $this->rest_base . '/customer-settings', [
[
@@ -104,15 +108,34 @@ class StoreController extends WP_REST_Controller {
'permission_callback' => [$this, 'check_permission'],
],
]);
// GET /woonoow/v1/store/security-settings
register_rest_route($this->namespace, '/' . $this->rest_base . '/security-settings', [
[
'methods' => WP_REST_Server::READABLE,
'callback' => [$this, 'get_security_settings'],
'permission_callback' => [$this, 'check_permission'],
],
]);
// POST /woonoow/v1/store/security-settings
register_rest_route($this->namespace, '/' . $this->rest_base . '/security-settings', [
[
'methods' => WP_REST_Server::EDITABLE,
'callback' => [$this, 'save_security_settings'],
'permission_callback' => [$this, 'check_permission'],
],
]);
}
/**
* Get store branding (PUBLIC - for login page)
*
* @param WP_REST_Request $request Request object
* @return WP_REST_Response Response object
*/
public function get_branding(WP_REST_Request $request) {
public function get_branding(WP_REST_Request $request)
{
$branding = [
'store_name' => get_option('woonoow_store_name', '') ?: get_option('blogname', 'WooNooW'),
'store_logo' => get_option('woonoow_store_logo', ''),
@@ -120,26 +143,27 @@ class StoreController extends WP_REST_Controller {
'store_icon' => get_option('woonoow_store_icon', ''),
'store_tagline' => get_option('woonoow_store_tagline', ''),
];
$response = rest_ensure_response($branding);
$response->header('Cache-Control', 'max-age=300'); // Cache for 5 minutes
return $response;
}
/**
* Get store settings
*
* @param WP_REST_Request $request Request object
* @return WP_REST_Response|WP_Error Response object or error
*/
public function get_settings(WP_REST_Request $request) {
public function get_settings(WP_REST_Request $request)
{
try {
$settings = StoreSettingsProvider::get_settings();
$response = rest_ensure_response($settings);
$response->header('Cache-Control', 'max-age=60');
return $response;
} catch (\Exception $e) {
return new WP_Error(
@@ -149,16 +173,17 @@ class StoreController extends WP_REST_Controller {
);
}
}
/**
* Save store settings
*
* @param WP_REST_Request $request Request object
* @return WP_REST_Response|WP_Error Response object or error
*/
public function save_settings(WP_REST_Request $request) {
public function save_settings(WP_REST_Request $request)
{
$settings = $request->get_json_params();
if (empty($settings)) {
return new WP_Error(
'missing_settings',
@@ -166,10 +191,10 @@ class StoreController extends WP_REST_Controller {
['status' => 400]
);
}
try {
$result = StoreSettingsProvider::save_settings($settings);
if (!$result) {
return new WP_Error(
'save_failed',
@@ -177,7 +202,7 @@ class StoreController extends WP_REST_Controller {
['status' => 500]
);
}
return rest_ensure_response([
'success' => true,
'message' => 'Settings saved successfully',
@@ -191,20 +216,21 @@ class StoreController extends WP_REST_Controller {
);
}
}
/**
* Get countries
*
* @param WP_REST_Request $request Request object
* @return WP_REST_Response|WP_Error Response object or error
*/
public function get_countries(WP_REST_Request $request) {
public function get_countries(WP_REST_Request $request)
{
try {
$countries = StoreSettingsProvider::get_countries();
$response = rest_ensure_response($countries);
$response->header('Cache-Control', 'max-age=3600'); // Cache for 1 hour
return $response;
} catch (\Exception $e) {
return new WP_Error(
@@ -214,20 +240,21 @@ class StoreController extends WP_REST_Controller {
);
}
}
/**
* Get timezones
*
* @param WP_REST_Request $request Request object
* @return WP_REST_Response|WP_Error Response object or error
*/
public function get_timezones(WP_REST_Request $request) {
public function get_timezones(WP_REST_Request $request)
{
try {
$timezones = StoreSettingsProvider::get_timezones();
$response = rest_ensure_response($timezones);
$response->header('Cache-Control', 'max-age=3600'); // Cache for 1 hour
return $response;
} catch (\Exception $e) {
return new WP_Error(
@@ -237,20 +264,21 @@ class StoreController extends WP_REST_Controller {
);
}
}
/**
* Get currencies
*
* @param WP_REST_Request $request Request object
* @return WP_REST_Response|WP_Error Response object or error
*/
public function get_currencies(WP_REST_Request $request) {
public function get_currencies(WP_REST_Request $request)
{
try {
$currencies = StoreSettingsProvider::get_currencies();
$response = rest_ensure_response($currencies);
$response->header('Cache-Control', 'max-age=3600'); // Cache for 1 hour
return $response;
} catch (\Exception $e) {
return new WP_Error(
@@ -260,20 +288,21 @@ class StoreController extends WP_REST_Controller {
);
}
}
/**
* Get customer settings
*
* @param WP_REST_Request $request Request object
* @return WP_REST_Response|WP_Error Response object or error
*/
public function get_customer_settings(WP_REST_Request $request) {
public function get_customer_settings(WP_REST_Request $request)
{
try {
$settings = CustomerSettingsProvider::get_settings();
$response = rest_ensure_response($settings);
$response->header('Cache-Control', 'max-age=60');
return $response;
} catch (\Exception $e) {
return new WP_Error(
@@ -283,17 +312,18 @@ class StoreController extends WP_REST_Controller {
);
}
}
/**
* Save customer settings
*
* @param WP_REST_Request $request Request object
* @return WP_REST_Response|WP_Error Response object or error
*/
public function save_customer_settings(WP_REST_Request $request) {
public function save_customer_settings(WP_REST_Request $request)
{
try {
$settings = $request->get_json_params();
if (empty($settings)) {
return new WP_Error(
'invalid_settings',
@@ -301,9 +331,9 @@ class StoreController extends WP_REST_Controller {
['status' => 400]
);
}
$updated = CustomerSettingsProvider::update_settings($settings);
if (!$updated) {
return new WP_Error(
'update_failed',
@@ -311,16 +341,15 @@ class StoreController extends WP_REST_Controller {
['status' => 500]
);
}
// Return updated settings
$new_settings = CustomerSettingsProvider::get_settings();
return new WP_REST_Response([
'success' => true,
'message' => __('Customer settings updated successfully', 'woonoow'),
'settings' => $new_settings,
], 200);
} catch (\Exception $e) {
return new WP_Error(
'save_customer_settings_failed',
@@ -329,13 +358,84 @@ class StoreController extends WP_REST_Controller {
);
}
}
/**
* Get security settings
*
* @param WP_REST_Request $request Request object
* @return WP_REST_Response|WP_Error Response object or error
*/
public function get_security_settings(WP_REST_Request $request)
{
try {
$settings = SecuritySettingsProvider::get_settings();
$response = rest_ensure_response($settings);
$response->header('Cache-Control', 'max-age=60');
return $response;
} catch (\Exception $e) {
return new WP_Error(
'get_security_settings_failed',
$e->getMessage(),
['status' => 500]
);
}
}
/**
* Save security settings
*
* @param WP_REST_Request $request Request object
* @return WP_REST_Response|WP_Error Response object or error
*/
public function save_security_settings(WP_REST_Request $request)
{
try {
$settings = $request->get_json_params();
if (empty($settings)) {
return new WP_Error(
'invalid_settings',
__('Invalid settings data', 'woonoow'),
['status' => 400]
);
}
$updated = SecuritySettingsProvider::update_settings($settings);
if (!$updated) {
return new WP_Error(
'update_failed',
__('Failed to update security settings', 'woonoow'),
['status' => 500]
);
}
// Return updated settings
$new_settings = SecuritySettingsProvider::get_settings();
return new WP_REST_Response([
'success' => true,
'message' => __('Security settings updated successfully', 'woonoow'),
'settings' => $new_settings,
], 200);
} catch (\Exception $e) {
return new WP_Error(
'save_security_settings_failed',
$e->getMessage(),
['status' => 500]
);
}
}
/**
* Check if user has permission
*
* @return bool True if user has permission
*/
public function check_permission() {
public function check_permission()
{
// Check WooCommerce capability first, fallback to manage_options
return current_user_can('manage_woocommerce') || current_user_can('manage_options');
}

View File

@@ -471,6 +471,31 @@ class SubscriptionsController
}
$enriched['billing_schedule'] = sprintf(__('Every %s%s', 'woonoow'), $interval, $period);
// Add payment method title
$payment_title = $subscription->payment_method; // Default to ID
// 1. Try from payment_meta (stored snapshot)
if (!empty($subscription->payment_meta)) {
$meta = json_decode($subscription->payment_meta, true);
if (isset($meta['method_title']) && !empty($meta['method_title'])) {
$payment_title = $meta['method_title'];
}
}
// 2. If it looks like an ID (no spaces, lowercase), try to get fresh title from gateway
if ($payment_title === $subscription->payment_method && function_exists('WC')) {
$gateways_handler = WC()->payment_gateways();
if ($gateways_handler) {
$gateways = $gateways_handler->payment_gateways();
if (isset($gateways[$subscription->payment_method])) {
$gw = $gateways[$subscription->payment_method];
$payment_title = $gw->get_title() ?: $gw->method_title;
}
}
}
$enriched['payment_method_title'] = $payment_title;
return $enriched;
}
}