feat: Newsletter system improvements and validation framework
- Fix: Marketing events now display in Staff notifications tab - Reorganize: Move Coupons to Marketing/Coupons for better organization - Add: Comprehensive email/phone validation with extensible filter hooks - Email validation with regex pattern (xxxx@xxxx.xx) - Phone validation with WhatsApp verification support - Filter hooks for external API integration (QuickEmailVerification, etc.) - Fix: Newsletter template routes now use centralized notification email builder - Add: Validation.php class for reusable validation logic - Add: VALIDATION_HOOKS.md documentation with integration examples - Add: NEWSLETTER_CAMPAIGN_PLAN.md architecture for future campaign system - Fix: API delete method call in Newsletter.tsx (delete -> del) - Remove: Duplicate EmailTemplates.tsx (using notification system instead) - Update: Newsletter controller to use centralized Validation class Breaking changes: - Coupons routes moved from /routes/Coupons to /routes/Marketing/Coupons - Legacy /coupons routes maintained for backward compatibility
This commit is contained in:
@@ -4,6 +4,7 @@ namespace WooNooW\API;
|
||||
use WP_REST_Request;
|
||||
use WP_REST_Response;
|
||||
use WP_Error;
|
||||
use WooNooW\Core\Validation;
|
||||
|
||||
class NewsletterController {
|
||||
const API_NAMESPACE = 'woonoow/v1';
|
||||
@@ -112,8 +113,11 @@ class NewsletterController {
|
||||
public static function subscribe(WP_REST_Request $request) {
|
||||
$email = sanitize_email($request->get_param('email'));
|
||||
|
||||
if (!is_email($email)) {
|
||||
return new WP_Error('invalid_email', 'Invalid email address', ['status' => 400]);
|
||||
// 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)
|
||||
|
||||
@@ -68,19 +68,12 @@ class ProductsController {
|
||||
* Register REST API routes
|
||||
*/
|
||||
public static function register_routes() {
|
||||
error_log('WooNooW ProductsController::register_routes() START');
|
||||
|
||||
// List products
|
||||
$callback = [__CLASS__, 'get_products'];
|
||||
$is_callable = is_callable($callback);
|
||||
error_log('WooNooW ProductsController: Callback is_callable: ' . ($is_callable ? 'YES' : 'NO'));
|
||||
|
||||
$result = register_rest_route('woonoow/v1', '/products', [
|
||||
register_rest_route('woonoow/v1', '/products', [
|
||||
'methods' => 'GET',
|
||||
'callback' => $callback,
|
||||
'callback' => [__CLASS__, 'get_products'],
|
||||
'permission_callback' => [Permissions::class, 'check_admin_permission'],
|
||||
]);
|
||||
error_log('WooNooW ProductsController: GET /products registered: ' . ($result ? 'SUCCESS' : 'FAILED'));
|
||||
|
||||
// Get single product
|
||||
register_rest_route('woonoow/v1', '/products/(?P<id>\d+)', [
|
||||
@@ -136,8 +129,6 @@ class ProductsController {
|
||||
* Get products list with filters
|
||||
*/
|
||||
public static function get_products(WP_REST_Request $request) {
|
||||
error_log('WooNooW ProductsController::get_products() CALLED - START');
|
||||
|
||||
try {
|
||||
$page = max(1, (int) $request->get_param('page'));
|
||||
$per_page = min(100, max(1, (int) ($request->get_param('per_page') ?: 20)));
|
||||
@@ -206,12 +197,7 @@ class ProductsController {
|
||||
foreach ($query->posts as $post) {
|
||||
$product = wc_get_product($post->ID);
|
||||
if ($product) {
|
||||
$formatted = self::format_product_list_item($product);
|
||||
// Debug: Log first product to verify structure
|
||||
if (empty($products)) {
|
||||
error_log('WooNooW Debug - First product data: ' . print_r($formatted, true));
|
||||
}
|
||||
$products[] = $formatted;
|
||||
$products[] = self::format_product_list_item($product);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,14 +214,10 @@ class ProductsController {
|
||||
$response->header('Cache-Control', 'no-cache, no-store, must-revalidate');
|
||||
$response->header('Pragma', 'no-cache');
|
||||
$response->header('Expires', '0');
|
||||
$response->header('X-WooNooW-Version', '2.0'); // Debug header
|
||||
|
||||
error_log('WooNooW ProductsController::get_products() CALLED - END SUCCESS');
|
||||
return $response;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
error_log('WooNooW ProductsController::get_products() ERROR: ' . $e->getMessage());
|
||||
error_log('WooNooW ProductsController::get_products() TRACE: ' . $e->getTraceAsString());
|
||||
return new WP_Error('products_error', $e->getMessage(), ['status' => 500]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,17 +268,14 @@ class StoreController extends WP_REST_Controller {
|
||||
* @return WP_REST_Response|WP_Error Response object or error
|
||||
*/
|
||||
public function get_customer_settings(WP_REST_Request $request) {
|
||||
error_log('WooNooW: get_customer_settings called');
|
||||
try {
|
||||
$settings = CustomerSettingsProvider::get_settings();
|
||||
error_log('WooNooW: Customer settings retrieved: ' . print_r($settings, true));
|
||||
|
||||
$response = rest_ensure_response($settings);
|
||||
$response->header('Cache-Control', 'max-age=60');
|
||||
|
||||
return $response;
|
||||
} catch (\Exception $e) {
|
||||
error_log('WooNooW: get_customer_settings exception: ' . $e->getMessage());
|
||||
return new WP_Error(
|
||||
'get_customer_settings_failed',
|
||||
$e->getMessage(),
|
||||
|
||||
@@ -39,46 +39,49 @@ class CustomerSettingsProvider {
|
||||
* @return bool
|
||||
*/
|
||||
public static function update_settings($settings) {
|
||||
$updated = true;
|
||||
|
||||
// General settings
|
||||
if (isset($settings['auto_register_members'])) {
|
||||
$updated = $updated && update_option('woonoow_auto_register_members', $settings['auto_register_members'] ? 'yes' : 'no');
|
||||
if (array_key_exists('auto_register_members', $settings)) {
|
||||
$value = !empty($settings['auto_register_members']) ? 'yes' : 'no';
|
||||
update_option('woonoow_auto_register_members', $value);
|
||||
}
|
||||
|
||||
if (isset($settings['multiple_addresses_enabled'])) {
|
||||
$updated = $updated && update_option('woonoow_multiple_addresses_enabled', $settings['multiple_addresses_enabled'] ? 'yes' : 'no');
|
||||
if (array_key_exists('multiple_addresses_enabled', $settings)) {
|
||||
$value = !empty($settings['multiple_addresses_enabled']) ? 'yes' : 'no';
|
||||
update_option('woonoow_multiple_addresses_enabled', $value);
|
||||
}
|
||||
|
||||
if (isset($settings['wishlist_enabled'])) {
|
||||
$updated = $updated && update_option('woonoow_wishlist_enabled', $settings['wishlist_enabled'] ? 'yes' : 'no');
|
||||
if (array_key_exists('wishlist_enabled', $settings)) {
|
||||
$value = !empty($settings['wishlist_enabled']) ? 'yes' : 'no';
|
||||
update_option('woonoow_wishlist_enabled', $value);
|
||||
}
|
||||
|
||||
// VIP settings
|
||||
if (isset($settings['vip_min_spent'])) {
|
||||
$updated = $updated && update_option('woonoow_vip_min_spent', floatval($settings['vip_min_spent']));
|
||||
update_option('woonoow_vip_min_spent', floatval($settings['vip_min_spent']));
|
||||
}
|
||||
|
||||
if (isset($settings['vip_min_orders'])) {
|
||||
$updated = $updated && update_option('woonoow_vip_min_orders', intval($settings['vip_min_orders']));
|
||||
update_option('woonoow_vip_min_orders', intval($settings['vip_min_orders']));
|
||||
}
|
||||
|
||||
if (isset($settings['vip_timeframe'])) {
|
||||
$timeframe = in_array($settings['vip_timeframe'], ['all', '30', '90', '365'])
|
||||
? $settings['vip_timeframe']
|
||||
: 'all';
|
||||
$updated = $updated && update_option('woonoow_vip_timeframe', $timeframe);
|
||||
update_option('woonoow_vip_timeframe', $timeframe);
|
||||
}
|
||||
|
||||
if (isset($settings['vip_require_both'])) {
|
||||
$updated = $updated && update_option('woonoow_vip_require_both', $settings['vip_require_both'] ? 'yes' : 'no');
|
||||
if (array_key_exists('vip_require_both', $settings)) {
|
||||
$value = !empty($settings['vip_require_both']) ? 'yes' : 'no';
|
||||
update_option('woonoow_vip_require_both', $value);
|
||||
}
|
||||
|
||||
if (isset($settings['vip_exclude_refunded'])) {
|
||||
$updated = $updated && update_option('woonoow_vip_exclude_refunded', $settings['vip_exclude_refunded'] ? 'yes' : 'no');
|
||||
if (array_key_exists('vip_exclude_refunded', $settings)) {
|
||||
$value = !empty($settings['vip_exclude_refunded']) ? 'yes' : 'no';
|
||||
update_option('woonoow_vip_exclude_refunded', $value);
|
||||
}
|
||||
|
||||
return $updated;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
218
includes/Core/Validation.php
Normal file
218
includes/Core/Validation.php
Normal file
@@ -0,0 +1,218 @@
|
||||
<?php
|
||||
namespace WooNooW\Core;
|
||||
|
||||
use WP_Error;
|
||||
|
||||
/**
|
||||
* Validation utilities for WooNooW
|
||||
*
|
||||
* Provides extensible validation for emails, phone numbers, and other data types
|
||||
* with filter hooks for external API integration.
|
||||
*
|
||||
* @package WooNooW\Core
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class Validation {
|
||||
|
||||
/**
|
||||
* Validate email address with extensible filter hooks
|
||||
*
|
||||
* @param string $email Email address to validate
|
||||
* @param string $context Context of validation (e.g., 'newsletter_subscribe', 'checkout', 'registration')
|
||||
* @return true|WP_Error True if valid, WP_Error if invalid
|
||||
*/
|
||||
public static function validate_email($email, $context = 'general') {
|
||||
$email = sanitize_email($email);
|
||||
|
||||
// Basic format validation
|
||||
if (!is_email($email)) {
|
||||
return new WP_Error('invalid_email', __('Invalid email address', 'woonoow'), ['status' => 400]);
|
||||
}
|
||||
|
||||
// Enhanced email validation with regex pattern (xxxx@xxxx.xx)
|
||||
if (!preg_match('/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/', $email)) {
|
||||
return new WP_Error('invalid_email_format', __('Email must be in format: xxxx@xxxx.xx', 'woonoow'), ['status' => 400]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter to validate email address.
|
||||
*
|
||||
* Allows addons to extend validation using external APIs like quickemailverification.com
|
||||
*
|
||||
* @param bool|WP_Error $is_valid True if valid, WP_Error if invalid
|
||||
* @param string $email The email address to validate
|
||||
* @param string $context The context of validation
|
||||
*
|
||||
* @since 1.0.0
|
||||
*
|
||||
* Example usage in addon:
|
||||
* ```php
|
||||
* add_filter('woonoow/validate_email', function($is_valid, $email, $context) {
|
||||
* if ($context !== 'newsletter_subscribe') return $is_valid;
|
||||
*
|
||||
* // Call external API (QuickEmailVerification)
|
||||
* $api_key = get_option('woonoow_quickemail_api_key');
|
||||
* if (!$api_key) return $is_valid;
|
||||
*
|
||||
* $response = wp_remote_get("https://api.quickemailverification.com/v1/verify?email={$email}&apikey={$api_key}");
|
||||
*
|
||||
* if (is_wp_error($response)) {
|
||||
* return $is_valid; // Fallback to basic validation on API error
|
||||
* }
|
||||
*
|
||||
* $data = json_decode(wp_remote_retrieve_body($response), true);
|
||||
*
|
||||
* if (isset($data['result']) && $data['result'] !== 'valid') {
|
||||
* return new WP_Error('email_verification_failed', 'Email address could not be verified: ' . ($data['reason'] ?? 'Unknown'));
|
||||
* }
|
||||
*
|
||||
* return true;
|
||||
* }, 10, 3);
|
||||
* ```
|
||||
*/
|
||||
$email_validation = apply_filters('woonoow/validate_email', true, $email, $context);
|
||||
|
||||
if (is_wp_error($email_validation)) {
|
||||
return $email_validation;
|
||||
}
|
||||
|
||||
if ($email_validation !== true) {
|
||||
return new WP_Error('email_validation_failed', __('Email validation failed', 'woonoow'), ['status' => 400]);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate phone number with extensible filter hooks
|
||||
*
|
||||
* @param string $phone Phone number to validate
|
||||
* @param string $context Context of validation (e.g., 'checkout', 'registration', 'shipping')
|
||||
* @param string $country_code Optional country code (e.g., 'ID', 'US')
|
||||
* @return true|WP_Error True if valid, WP_Error if invalid
|
||||
*/
|
||||
public static function validate_phone($phone, $context = 'general', $country_code = '') {
|
||||
$phone = sanitize_text_field($phone);
|
||||
|
||||
// Remove common formatting characters
|
||||
$clean_phone = preg_replace('/[\s\-\(\)\.]+/', '', $phone);
|
||||
|
||||
// Basic validation: must contain only digits, +, and be at least 8 characters
|
||||
if (!preg_match('/^\+?[0-9]{8,15}$/', $clean_phone)) {
|
||||
return new WP_Error('invalid_phone', __('Phone number must be 8-15 digits and may start with +', 'woonoow'), ['status' => 400]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter to validate phone number.
|
||||
*
|
||||
* Allows addons to extend validation using external APIs or WhatsApp verification
|
||||
*
|
||||
* @param bool|WP_Error $is_valid True if valid, WP_Error if invalid
|
||||
* @param string $phone The phone number to validate (cleaned)
|
||||
* @param string $context The context of validation
|
||||
* @param string $country_code Country code if available
|
||||
*
|
||||
* @since 1.0.0
|
||||
*
|
||||
* Example usage for WhatsApp verification:
|
||||
* ```php
|
||||
* add_filter('woonoow/validate_phone', function($is_valid, $phone, $context, $country_code) {
|
||||
* if ($context !== 'checkout') return $is_valid;
|
||||
*
|
||||
* // Check if number is registered on WhatsApp
|
||||
* $api_key = get_option('woonoow_whatsapp_verify_api_key');
|
||||
* if (!$api_key) return $is_valid;
|
||||
*
|
||||
* $response = wp_remote_post('https://api.whatsapp.com/v1/contacts', [
|
||||
* 'headers' => ['Authorization' => 'Bearer ' . $api_key],
|
||||
* 'body' => json_encode(['blocking' => 'wait', 'contacts' => [$phone]]),
|
||||
* ]);
|
||||
*
|
||||
* if (is_wp_error($response)) {
|
||||
* return $is_valid; // Fallback on API error
|
||||
* }
|
||||
*
|
||||
* $data = json_decode(wp_remote_retrieve_body($response), true);
|
||||
*
|
||||
* if (!isset($data['contacts'][0]['wa_id'])) {
|
||||
* return new WP_Error('phone_not_whatsapp', 'Phone number is not registered on WhatsApp');
|
||||
* }
|
||||
*
|
||||
* return true;
|
||||
* }, 10, 4);
|
||||
* ```
|
||||
*
|
||||
* Example usage for general phone validation API:
|
||||
* ```php
|
||||
* add_filter('woonoow/validate_phone', function($is_valid, $phone, $context, $country_code) {
|
||||
* // Use numverify.com or similar service
|
||||
* $api_key = get_option('woonoow_numverify_api_key');
|
||||
* if (!$api_key) return $is_valid;
|
||||
*
|
||||
* $response = wp_remote_get("http://apilayer.net/api/validate?access_key={$api_key}&number={$phone}&country_code={$country_code}");
|
||||
*
|
||||
* if (is_wp_error($response)) return $is_valid;
|
||||
*
|
||||
* $data = json_decode(wp_remote_retrieve_body($response), true);
|
||||
*
|
||||
* if (!$data['valid']) {
|
||||
* return new WP_Error('phone_invalid', 'Phone number validation failed: ' . ($data['error'] ?? 'Invalid number'));
|
||||
* }
|
||||
*
|
||||
* return true;
|
||||
* }, 10, 4);
|
||||
* ```
|
||||
*/
|
||||
$phone_validation = apply_filters('woonoow/validate_phone', true, $clean_phone, $context, $country_code);
|
||||
|
||||
if (is_wp_error($phone_validation)) {
|
||||
return $phone_validation;
|
||||
}
|
||||
|
||||
if ($phone_validation !== true) {
|
||||
return new WP_Error('phone_validation_failed', __('Phone number validation failed', 'woonoow'), ['status' => 400]);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate phone number and check WhatsApp registration
|
||||
*
|
||||
* Convenience method that validates phone and checks WhatsApp in one call
|
||||
*
|
||||
* @param string $phone Phone number to validate
|
||||
* @param string $context Context of validation
|
||||
* @param string $country_code Optional country code
|
||||
* @return true|WP_Error True if valid and registered on WhatsApp, WP_Error otherwise
|
||||
*/
|
||||
public static function validate_phone_whatsapp($phone, $context = 'general', $country_code = '') {
|
||||
// First validate the phone number format
|
||||
$validation = self::validate_phone($phone, $context, $country_code);
|
||||
|
||||
if (is_wp_error($validation)) {
|
||||
return $validation;
|
||||
}
|
||||
|
||||
// Clean phone for WhatsApp check
|
||||
$clean_phone = preg_replace('/[\s\-\(\)\.]+/', '', $phone);
|
||||
|
||||
/**
|
||||
* Filter to check if phone is registered on WhatsApp
|
||||
*
|
||||
* @param bool|WP_Error $is_registered True if registered, WP_Error if not or error
|
||||
* @param string $phone The phone number (cleaned)
|
||||
* @param string $context The context of validation
|
||||
* @param string $country_code Country code if available
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
$whatsapp_check = apply_filters('woonoow/validate_phone_whatsapp', true, $clean_phone, $context, $country_code);
|
||||
|
||||
if (is_wp_error($whatsapp_check)) {
|
||||
return $whatsapp_check;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user