[ 'name' => __('Campaigns', 'woonoow'), 'singular_name' => __('Campaign', 'woonoow'), ], 'public' => false, 'show_ui' => false, 'show_in_rest' => false, 'supports' => ['title'], 'capability_type' => 'post', 'map_meta_cap' => true, ]); } /** * Create a new campaign * * @param array $data Campaign data * @return int|WP_Error Campaign ID or error */ public static function create($data) { $post_data = [ 'post_type' => self::POST_TYPE, 'post_status' => 'publish', 'post_title' => sanitize_text_field($data['title'] ?? 'Untitled Campaign'), ]; $campaign_id = wp_insert_post($post_data, true); if (is_wp_error($campaign_id)) { return $campaign_id; } // Save meta fields self::update_meta($campaign_id, $data); return $campaign_id; } /** * Update campaign * * @param int $campaign_id Campaign ID * @param array $data Campaign data * @return bool|WP_Error */ public static function update($campaign_id, $data) { $post = get_post($campaign_id); if (!$post || $post->post_type !== self::POST_TYPE) { return new \WP_Error('invalid_campaign', __('Campaign not found', 'woonoow')); } // Update title if provided if (isset($data['title'])) { wp_update_post([ 'ID' => $campaign_id, 'post_title' => sanitize_text_field($data['title']), ]); } // Update meta fields self::update_meta($campaign_id, $data); return true; } /** * Update campaign meta * * @param int $campaign_id * @param array $data */ private static function update_meta($campaign_id, $data) { $meta_fields = [ 'subject' => '_wnw_subject', 'content' => '_wnw_content', 'status' => '_wnw_status', 'scheduled_at' => '_wnw_scheduled_at', ]; foreach ($meta_fields as $key => $meta_key) { if (isset($data[$key])) { $value = $data[$key]; // Sanitize based on field type if ($key === 'content') { $value = wp_kses_post($value); } elseif ($key === 'scheduled_at') { $value = sanitize_text_field($value); } elseif ($key === 'status') { $allowed = ['draft', 'scheduled', 'sending', 'sent', 'failed']; $value = in_array($value, $allowed) ? $value : 'draft'; } else { $value = sanitize_text_field($value); } update_post_meta($campaign_id, $meta_key, $value); } } // Set default status if not provided if (!get_post_meta($campaign_id, '_wnw_status', true)) { update_post_meta($campaign_id, '_wnw_status', 'draft'); } } /** * Get campaign by ID * * @param int $campaign_id * @return array|null */ public static function get($campaign_id) { $post = get_post($campaign_id); if (!$post || $post->post_type !== self::POST_TYPE) { return null; } return self::format_campaign($post); } /** * Get all campaigns * * @param array $args Query args * @return array */ public static function get_all($args = []) { $defaults = [ 'post_type' => self::POST_TYPE, 'post_status' => 'any', 'posts_per_page' => -1, 'orderby' => 'date', 'order' => 'DESC', ]; $query_args = wp_parse_args($args, $defaults); $query_args['post_type'] = self::POST_TYPE; // Force post type $posts = get_posts($query_args); return array_map([__CLASS__, 'format_campaign'], $posts); } /** * Format campaign post to array * * @param WP_Post $post * @return array */ private static function format_campaign($post) { return [ 'id' => $post->ID, 'title' => $post->post_title, 'subject' => get_post_meta($post->ID, '_wnw_subject', true), 'content' => get_post_meta($post->ID, '_wnw_content', true), 'status' => get_post_meta($post->ID, '_wnw_status', true) ?: 'draft', 'scheduled_at' => get_post_meta($post->ID, '_wnw_scheduled_at', true), 'sent_at' => get_post_meta($post->ID, '_wnw_sent_at', true), 'recipient_count' => (int) get_post_meta($post->ID, '_wnw_recipient_count', true), 'sent_count' => (int) get_post_meta($post->ID, '_wnw_sent_count', true), 'failed_count' => (int) get_post_meta($post->ID, '_wnw_failed_count', true), 'created_at' => $post->post_date, 'updated_at' => $post->post_modified, ]; } /** * Delete campaign * * @param int $campaign_id * @return bool */ public static function delete($campaign_id) { $post = get_post($campaign_id); if (!$post || $post->post_type !== self::POST_TYPE) { return false; } return wp_delete_post($campaign_id, true) !== false; } /** * Send campaign * * @param int $campaign_id * @return array Result with sent/failed counts */ public static function send($campaign_id) { $campaign = self::get($campaign_id); if (!$campaign) { return ['success' => false, 'error' => __('Campaign not found', 'woonoow')]; } if ($campaign['status'] === 'sent') { return ['success' => false, 'error' => __('Campaign already sent', 'woonoow')]; } // Get subscribers $subscribers = self::get_subscribers(); if (empty($subscribers)) { return ['success' => false, 'error' => __('No subscribers to send to', 'woonoow')]; } // Update status to sending update_post_meta($campaign_id, '_wnw_status', 'sending'); update_post_meta($campaign_id, '_wnw_recipient_count', count($subscribers)); $sent = 0; $failed = 0; // Get email template $template = self::render_campaign_email($campaign); // Send in batches $batch_size = 50; $batches = array_chunk($subscribers, $batch_size); foreach ($batches as $batch) { foreach ($batch as $subscriber) { $email = $subscriber['email']; // Replace subscriber-specific variables $body = str_replace('{subscriber_email}', $email, $template['body']); $body = str_replace('{unsubscribe_url}', self::get_unsubscribe_url($email), $body); // Send email $result = wp_mail( $email, $template['subject'], $body, ['Content-Type: text/html; charset=UTF-8'] ); if ($result) { $sent++; } else { $failed++; } } // Small delay between batches if (count($batches) > 1) { sleep(2); } } // Update campaign stats update_post_meta($campaign_id, '_wnw_sent_count', $sent); update_post_meta($campaign_id, '_wnw_failed_count', $failed); update_post_meta($campaign_id, '_wnw_sent_at', current_time('mysql')); update_post_meta($campaign_id, '_wnw_status', $failed > 0 && $sent === 0 ? 'failed' : 'sent'); return [ 'success' => true, 'sent' => $sent, 'failed' => $failed, 'total' => count($subscribers), ]; } /** * Send test email * * @param int $campaign_id * @param string $email Test email address * @return bool */ public static function send_test($campaign_id, $email) { $campaign = self::get($campaign_id); if (!$campaign) { return false; } $template = self::render_campaign_email($campaign); // Replace subscriber-specific variables $body = str_replace('{subscriber_email}', $email, $template['body']); $body = str_replace('{unsubscribe_url}', '#', $body); return wp_mail( $email, '[TEST] ' . $template['subject'], $body, ['Content-Type: text/html; charset=UTF-8'] ); } /** * Render campaign email using EmailRenderer * * @param array $campaign * @return array ['subject' => string, 'body' => string] */ private static function render_campaign_email($campaign) { $renderer = \WooNooW\Core\Notifications\EmailRenderer::instance(); // Get the campaign email template $template = $renderer->get_template_settings('newsletter_campaign', 'customer'); // Fallback if no template configured if (!$template) { $subject = $campaign['subject'] ?: $campaign['title']; $body = $campaign['content']; } else { $subject = $template['subject'] ?: $campaign['subject']; // Replace {content} with campaign content $body = str_replace('{content}', $campaign['content'], $template['body']); // Replace {campaign_title} $body = str_replace('{campaign_title}', $campaign['title'], $body); } // Replace common variables $site_name = get_bloginfo('name'); $site_url = home_url(); $subject = str_replace(['{site_name}', '{store_name}'], $site_name, $subject); $body = str_replace(['{site_name}', '{store_name}'], $site_name, $body); $body = str_replace('{site_url}', $site_url, $body); $body = str_replace('{current_date}', date_i18n(get_option('date_format')), $body); $body = str_replace('{current_year}', date('Y'), $body); // Render through email design template $design_path = $renderer->get_design_template(); if (file_exists($design_path)) { $body = $renderer->render_html($design_path, $body, $subject, [ 'site_name' => $site_name, 'site_url' => $site_url, ]); } return [ 'subject' => $subject, 'body' => $body, ]; } /** * Get subscribers * * @return array */ private static function get_subscribers() { // Check if using custom table $use_table = !get_option('woonoow_newsletter_limit_enabled', true); if ($use_table && self::has_subscribers_table()) { global $wpdb; $table = $wpdb->prefix . 'woonoow_subscribers'; return $wpdb->get_results( "SELECT email, user_id FROM {$table} WHERE status = 'active'", ARRAY_A ); } // Use wp_options storage $subscribers = get_option('woonoow_newsletter_subscribers', []); return array_filter($subscribers, function($sub) { return ($sub['status'] ?? 'active') === 'active'; }); } /** * Check if subscribers table exists * * @return bool */ private static function has_subscribers_table() { global $wpdb; $table = $wpdb->prefix . 'woonoow_subscribers'; return $wpdb->get_var("SHOW TABLES LIKE '{$table}'") === $table; } /** * Get unsubscribe URL * * @param string $email * @return string */ private static function get_unsubscribe_url($email) { $token = wp_hash($email . 'woonoow_unsubscribe'); return add_query_arg([ 'woonoow_unsubscribe' => 1, 'email' => urlencode($email), 'token' => $token, ], home_url()); } /** * Process scheduled campaigns (WP-Cron) */ public static function process_scheduled_campaigns() { // Only if scheduling is enabled if (!get_option('woonoow_campaign_scheduling_enabled', false)) { return; } $campaigns = self::get_all([ 'meta_query' => [ [ 'key' => '_wnw_status', 'value' => 'scheduled', ], [ 'key' => '_wnw_scheduled_at', 'value' => current_time('mysql'), 'compare' => '<=', 'type' => 'DATETIME', ], ], ]); foreach ($campaigns as $campaign) { self::send($campaign['id']); } } /** * Enable scheduling (registers cron) */ public static function enable_scheduling() { if (!wp_next_scheduled(self::CRON_HOOK)) { wp_schedule_event(time(), 'hourly', self::CRON_HOOK); } } /** * Disable scheduling (clears cron) */ public static function disable_scheduling() { wp_clear_scheduled_hook(self::CRON_HOOK); } }