--- a/email-subscribers/email-subscribers.php
+++ b/email-subscribers/email-subscribers.php
@@ -3,7 +3,7 @@
* Plugin Name: Icegram Express - Email Subscribers, Newsletters and Marketing Automation Plugin
* Plugin URI: https://www.icegram.com/
* Description: Add subscription forms on website, send HTML newsletters & automatically notify subscribers about new blog posts once it is published.
- * Version: 5.9.16
+ * Version: 5.9.17
* Author: Icegram
* Author URI: https://www.icegram.com/
* Requires at least: 3.9
@@ -187,7 +187,7 @@
/* ***************************** Initial Compatibility Work (End) ******************* */
if ( ! defined( 'ES_PLUGIN_VERSION' ) ) {
- define( 'ES_PLUGIN_VERSION', '5.9.16' );
+ define( 'ES_PLUGIN_VERSION', '5.9.17' );
}
// Plugin Folder Path.
--- a/email-subscribers/lite/includes/controllers/class-es-campaign-controller.php
+++ b/email-subscribers/lite/includes/controllers/class-es-campaign-controller.php
@@ -314,11 +314,20 @@
$email_queued = self::queue_emails( $mailing_queue_id, $mailing_queue_hash, $campaign_id );
if ( $email_queued ) {
$response['success'] = true;
- $response['data']['redirect_url'] = admin_url( 'admin.php?page=es_campaigns&id=' . $campaign_id . '&action=campaign_scheduled' );
+ $response['data']['redirect_url'] = admin_url( 'admin.php?page=es_dashboard#campaigns' );
+
+ // Check if this is immediate send (schedule_now) to trigger sending from frontend
+ $scheduling_option = ! empty( $campaign_data['scheduling_option'] ) ? $campaign_data['scheduling_option'] : 'schedule_now';
+ if ( 'schedule_now' === $scheduling_option ) {
+ $response['data']['should_trigger_sending'] = true;
+ $response['data']['mailing_queue_id'] = $mailing_queue_id;
+ $response['data']['mailing_queue_hash'] = $mailing_queue_hash;
+ } else {
+ // For scheduled campaigns, WordPress cron will handle sending
+ $response['data']['should_trigger_sending'] = false;
+ }
}
}
-
- self::maybe_send_mailing_queue( $mailing_queue_id, $mailing_queue_hash );
}
}
@@ -581,6 +590,36 @@
}
}
+ /**
+ * New API endpoint to trigger mailing queue sending from frontend
+ * Called after successful campaign scheduling
+ *
+ * @param array $data Contains mailing_queue_id and mailing_queue_hash
+ * @return array Response with success status
+ *
+ * @since 5.9.17
+ */
+ public static function trigger_mailing_queue( $data ) {
+ $data = ES_Common::decode_args( $data );
+
+ $response = array(
+ 'success' => false,
+ 'message' => __( 'Invalid parameters', 'email-subscribers' )
+ );
+
+ $mailing_queue_id = ! empty( $data['mailing_queue_id'] ) ? absint( $data['mailing_queue_id'] ) : 0;
+ $mailing_queue_hash = ! empty( $data['mailing_queue_hash'] ) ? sanitize_text_field( $data['mailing_queue_hash'] ) : '';
+
+ if ( ! empty( $mailing_queue_id ) && ! empty( $mailing_queue_hash ) ) {
+ // Trigger the mailing queue sending
+ self::maybe_send_mailing_queue( $mailing_queue_id, $mailing_queue_hash );
+
+ $response['success'] = true;
+ $response['message'] = __( 'Campaign sending triggered successfully', 'email-subscribers' );
+ }
+
+ return $response;
+ }
public static function is_using_new_category_format( $campaign_id ) {
$new_flow_campaign_ids = get_option( 'ig_es_new_category_format_campaign_ids', array() );
$using_new_category_format = false;
--- a/email-subscribers/lite/includes/controllers/class-es-contact-controller.php
+++ b/email-subscribers/lite/includes/controllers/class-es-contact-controller.php
@@ -84,31 +84,33 @@
// Validate list selection.
$lists = is_array( $contact_data['lists'] ) ? $contact_data['lists'] : array();
- if ( empty( $lists ) ) {
- $errors[] = esc_html__( 'Please select a list', 'email-subscribers' );
- }
-
- // Check for duplicate contact.
- if ( ! empty( $email ) ) {
- $existing_contact_id = ES()->contacts_db->get_contact_id_by_email( $email );
- if ( $existing_contact_id && ( (int) $existing_contact_id !== (int) $contact_data['id'] ) ) {
- $errors[] = esc_html__( 'Contact already exists.', 'email-subscribers' );
- }
+ // Only require list selection when creating a new contact
+ $is_new_contact = empty( $contact_data['id'] );
+ if ( empty( $lists ) && $is_new_contact ) {
+ $errors[] = esc_html__( 'Please select a list', 'email-subscribers' );
+ }
+
+ // Check for duplicate contact.
+ if ( ! empty( $email ) ) {
+ $existing_contact_id = ES()->contacts_db->get_contact_id_by_email( $email );
+ if ( $existing_contact_id && ( (int) $existing_contact_id !== (int) $contact_data['id'] ) ) {
+ $errors[] = esc_html__( 'Contact already exists.', 'email-subscribers' );
}
-
- // Return result.
- return array(
- 'errors' => $errors,
- 'email' => $email,
- 'lists' => $lists,
- 'contact' => array(
- 'first_name' => sanitize_text_field( $contact_data['first_name'] ),
- 'last_name' => sanitize_text_field( $contact_data['last_name'] ),
- 'email' => $email,
- 'status' => 'verified',
- ),
- );
}
+
+ // Return result.
+ return array(
+ 'errors' => $errors,
+ 'email' => $email,
+ 'lists' => $lists,
+ 'contact' => array(
+ 'first_name' => sanitize_text_field( $contact_data['first_name'] ),
+ 'last_name' => sanitize_text_field( $contact_data['last_name'] ),
+ 'email' => $email,
+ 'status' => 'verified',
+ ),
+ );
+ }
public static function maybe_send_welcome_email( $contact_data, $contact, $list_ids ) {
--- a/email-subscribers/lite/includes/controllers/class-es-contact-import-controller.php
+++ b/email-subscribers/lite/includes/controllers/class-es-contact-import-controller.php
@@ -1225,7 +1225,7 @@
ES_Cache::flush();
wp_cache_flush();
- if ( 'yes' !== $send_optin_emails ) {
+ if ( 'yes' === $send_optin_emails ) {
try {
$subscriber_options = array();
$subscriber_options[ $contact_id ]['type'] = 'optin_welcome_email';
--- a/email-subscribers/lite/includes/controllers/class-es-contacts-controller.php
+++ b/email-subscribers/lite/includes/controllers/class-es-contacts-controller.php
@@ -95,7 +95,7 @@
// Convert frontend filters to ES advanced filter format
$condition = self::build_es_filter_condition( $field, $operator, $value );
if ( $condition ) {
- $advanced_filter_conditions[] = $condition;
+ $advanced_filter_conditions[] = $condition;
}
}
@@ -174,46 +174,38 @@
// Handle List filtering (from advanced_filter and filter_by_list_id)
- $all_list_ids = array();
-
- // Add List filters from advanced_filter
if ( ! empty( $list_filters ) ) {
+ // Process list filters with operator support
foreach ( $list_filters as $list_filter ) {
+ $list_values = array();
if ( is_array( $list_filter['value'] ) ) {
- $all_list_ids = array_merge( $all_list_ids, $list_filter['value'] );
+ $list_values = $list_filter['value'];
} else {
- $all_list_ids[] = $list_filter['value'];
+ $list_values[] = $list_filter['value'];
+ }
+
+ $list_operator = isset( $list_filter['operator'] ) ? $list_filter['operator'] : 'is equal to';
+
+ if ( ! empty( $list_values ) ) {
+ // Get contact IDs from lists_contacts table with operator support
+ $list_filtered_contact_ids = $lists_contacts_db->get_contact_ids_by_list_operator( $list_values, $list_operator );
+
+ if ( ! empty( $filtered_contact_ids ) ) {
+ // Intersect with existing contact IDs from other advanced filters
+ $filtered_contact_ids = array_intersect( $filtered_contact_ids, $list_filtered_contact_ids );
+ } else {
+ // Only list filtering
+ $filtered_contact_ids = $list_filtered_contact_ids;
+ }
+
+ if ( empty( $filtered_contact_ids ) ) {
+ return $do_count_only ? 0 : array();
+ }
}
}
- }
-
- // Add filter_by_list_id if present
- if ( ! empty( $filter_by_list_id ) && $filter_by_list_id !== 'all' ) {
- $all_list_ids[] = intval( $filter_by_list_id );
- }
-
- // Apply list filtering
- if ( ! empty( $all_list_ids ) ) {
- $list_ids = array_unique( array_map( 'intval', $all_list_ids ) );
- // Pass list_ids to DB query to leverage join for performance
- $query_args['list_ids'] = $list_ids;
-
- // Get contact IDs from lists_contacts table via DB class (supports multiple list IDs)
- $list_filtered_contact_ids = $lists_contacts_db->get_contact_ids_by_criteria( $list_ids );
-
- if ( ! empty( $filtered_contact_ids ) ) {
- // Intersect with existing contact IDs from advanced filters
- $before_count = count( $filtered_contact_ids );
- $filtered_contact_ids = array_intersect( $filtered_contact_ids, $list_filtered_contact_ids );
- $after_count = count( $filtered_contact_ids );
- } else {
- // Only list filtering
- $filtered_contact_ids = $list_filtered_contact_ids;
- }
-
- if ( empty( $filtered_contact_ids ) ) {
- return $do_count_only ? 0 : array();
- }
+ } else if ( ! empty( $filter_by_list_id ) && $filter_by_list_id !== 'all' ) {
+ // Handle simple list filter (dropdown without advanced filters)
+ $query_args['list_ids'] = array( intval( $filter_by_list_id ) );
}
// Add filtered contact IDs to query args
@@ -691,7 +683,6 @@
'Email' => 'subscribers.email',
'Country' => 'subscribers.country_code',
'Bounce Status' => 'subscribers.bounce_status',
- 'Subscribed' => 'subscribers.created_at',
'Engagement Score' => 'subscribers.engagement_score',
'Status' => 'subscribers.status',
);
@@ -752,12 +743,6 @@
return intval($value); // Convert to integer
}
- case 'Subscribed':
- if (is_string($value) && strtotime($value)) {
- return date('Y-m-d', strtotime($value));
- }
- return $value;
-
case 'has received':
if (is_array($value) && isset($value['id'])) {
return $value['id'];
--- a/email-subscribers/lite/includes/controllers/class-es-dashboard-controller.php
+++ b/email-subscribers/lite/includes/controllers/class-es-dashboard-controller.php
@@ -56,15 +56,19 @@
$days = 7;
} else {
$days = intval( $days );
+ if ( $days < 1 || $days > 365 ) {
+ $days = 7;
+ }
}
- if ( ! empty( $list_id ) && $list_id !== 'all' && ! is_numeric( $list_id ) ) {
- $list_id = 0;
- }
-
if ( $list_id === 'all' ) {
$list_id = 0;
+ } elseif ( ! empty( $list_id ) && ! is_numeric( $list_id ) ) {
+ $list_id = '';
+ } elseif ( ! empty( $list_id ) ) {
+ $list_id = intval( $list_id );
}
+ // If $list_id is empty, leave it as empty string (default behavior)
$reports_args = array(
'list_id' => $list_id,
@@ -103,40 +107,52 @@
);
$forms = ES()->forms_db->get_forms( $forms_args );
- if ( ! empty( $forms ) ) {
- foreach ( $forms as &$form ) {
- $form_id = ! empty( $form['id'] ) ? intval( $form['id'] ) : 0;
-
- $form['subscriber_count'] = ES()->contacts_db->get_total_contacts_by_form_id( $form_id, 0 );
-
- $settings = ! empty( $form['settings'] ) ? ig_es_maybe_unserialize( $form['settings'] ) : [];
-
- $list_ids = [];
+ if ( ! empty( $forms ) && is_array( $forms ) ) {
+ $form_ids = array_column( $forms, 'id' );
+ $form_ids = array_filter( $form_ids, 'is_numeric' );
+
+ if ( ! empty( $form_ids ) ) {
+ $all_list_ids = array();
+
+ foreach ( $forms as $form ) {
+ $settings = ! empty( $form['settings'] ) ? ig_es_maybe_unserialize( $form['settings'] ) : array();
if ( ! empty( $settings['lists'] ) ) {
- $list_ids = is_array( $settings['lists'] ) ? $settings['lists'] : [ intval( $settings['lists'] ) ];
+ $list_ids = is_array( $settings['lists'] ) ? $settings['lists'] : array( intval( $settings['lists'] ) );
+ $all_list_ids = array_merge( $all_list_ids, $list_ids );
}
-
- $list_names = [];
- if ( ! empty( $list_ids ) ) {
- foreach ( $list_ids as $list_id ) {
- $list_names[] = ES()->lists_db->get_list_name_by_id( $list_id );
+ }
+ $all_list_ids = array_unique( $all_list_ids );
+
+ $subscriber_counts = ES()->contacts_db->get_subscriber_counts_by_form_ids( $form_ids );
+ $list_names_map = ! empty( $all_list_ids ) ? ES()->lists_db->get_list_name_by_ids( $all_list_ids ) : array();
+
+ foreach ( $forms as &$form ) {
+ $form_id = intval( $form['id'] );
+ $form['subscriber_count'] = isset( $subscriber_counts[ $form_id ] ) ? $subscriber_counts[ $form_id ] : 0;
+
+ $settings = ! empty( $form['settings'] ) ? ig_es_maybe_unserialize( $form['settings'] ) : array();
+ $list_ids = ! empty( $settings['lists'] ) ? ( is_array( $settings['lists'] ) ? $settings['lists'] : array( intval( $settings['lists'] ) ) ) : array();
+
+ $list_names = array();
+ foreach ( $list_ids as $list_id ) {
+ if ( isset( $list_names_map[ $list_id ] ) ) {
+ $list_names[] = $list_names_map[ $list_id ];
}
}
-
- $form['list_names'] = ! empty( $list_names ) ? implode( ', ', $list_names ) : '';
+ $form['list_names'] = implode( ', ', $list_names );
}
-
}
+ }
- $lists = array_slice( array_reverse( ES()->lists_db->get_lists() ), 0, 2 );
- $workflows = ES()->workflows_db->get_workflows();
+ $lists = ES()->lists_db->get_lists( array( 'order_by' => 'id', 'order' => 'DESC', 'limit' => 2 ) );
+ $workflows_count = ES()->workflows_db->get_workflows_count();
$onboarding_tasks_status = array(
'sendFirstCampaign' => ! empty( $campaigns ) ? 'yes' : 'no',
'importContacts' => ! empty( $dashboard_kpi['total_subscribers'] ) && $dashboard_kpi['total_subscribers'] > 0 ? 'yes' : 'no',
'createSubscriptionFormDone' => ! empty( $forms ) ? 'yes' : 'no',
- 'createWorkflowDone' => ! empty( $workflows ) ? 'yes' : 'no'
+ 'createWorkflowDone' => $workflows_count > 0 ? 'yes' : 'no'
);
$icegram_plugins = self::get_icegram_plugins_info();
@@ -312,7 +328,13 @@
return $plugins_info;
}
- public static function get_audience_activities() {
+ /**
+ * Get audience activity feed with recent subscribe/unsubscribe actions
+ *
+ * @return array Recent activities with caching
+ * @since 4.0.0
+ */
+ public static function get_audience_activities() {
$cache_key = 'es_audience_activities';
$cached = ES_Cache::get_transient( $cache_key );
if ( false !== $cached ) {
@@ -337,7 +359,14 @@
return $recent_activities;
}
- public static function prepare_activities_from_actions( $actions ) {
+ /**
+ * Prepare activity feed from raw action records
+ *
+ * @param array $actions Raw action records from database
+ * @return array Formatted activities for display
+ * @since 4.0.0
+ */
+ public static function prepare_activities_from_actions( $actions ) {
$activities = array();
if ( $actions ) {
$contact_ids = array_column( $actions, 'contact_id' );
@@ -372,14 +401,16 @@
$contact_info_text = $contact_email;
}
- $contact_info_text = '<a href="?page=es_subscribers&action=edit&subscriber=' . $contact_id . '" class="text-indigo-600" target="_blank">' . $contact_info_text . '</a>';
- $action_verb = ES()->actions->get_action_verb( $action_type );
- $action_created_at = $action['created_at'];
- $activity_time = human_time_diff( time(), $action_created_at ) . ' ' . __( 'ago', 'email-subscribers' );
-
- $list_id = ! empty( $action['list_id'] ) ? $action['list_id'] : 0;
- $list_name = ! empty( $lists_name[ $list_id ] ) ? $lists_name[ $list_id ] : '';
- $action_obj_name = '<a href="?page=es_lists&action=edit&list=' . $list_id . '" target="_blank">' . $list_name . '</a> ' . __( 'list', 'email-subscribers' );
+ $contact_url = esc_url( admin_url( 'admin.php?page=es_dashboard#/audience' ) );
+ $contact_info_text = '<a href="' . $contact_url . '" class="text-indigo-600" target="_blank">' . esc_html( $contact_info_text ) . '</a>';
+ $action_verb = ES()->actions->get_action_verb( $action_type );
+ $action_created_at = $action['created_at'];
+ $activity_time = human_time_diff( $action_created_at, time() ) . ' ' . __( 'ago', 'email-subscribers' );
+
+ $list_id = ! empty( $action['list_id'] ) ? $action['list_id'] : 0;
+ $list_name = ! empty( $lists_name[ $list_id ] ) ? $lists_name[ $list_id ] : '';
+ $list_url = esc_url( add_query_arg( array( 'page' => 'es_lists', 'action' => 'edit', 'list' => $list_id ), admin_url( 'admin.php' ) ) );
+ $action_obj_name = '<a href="' . $list_url . '" target="_blank">' . esc_html( $list_name ) . '</a> ' . esc_html__( 'list', 'email-subscribers' );
$activity_text = $contact_info_text . ' ' . $action_verb . ' ' . $action_obj_name;
$activities[] = array(
'time' => $activity_time,
@@ -391,7 +422,14 @@
return $activities;
}
- public static function get_recent_campaigns_kpis( $campaign_id ) {
+ /**
+ * Get campaign performance KPIs (open rate, click rate, sent count)
+ *
+ * @param int $campaign_id Campaign ID
+ * @return array Campaign statistics
+ * @since 4.0.0
+ */
+ public static function get_recent_campaigns_kpis( $campaign_id ) {
$args = array(
'campaign_id' => $campaign_id,
'types' => array(
@@ -401,25 +439,28 @@
)
);
$actions_count = ES()->actions_db->get_actions_count( $args );
- $total_email_sent = $actions_count['sent'];
- $total_email_opened = $actions_count['opened'];
- $total_email_clicked = $actions_count['clicked'];
- $open_rate = ! empty( $total_email_sent ) ? number_format_i18n( ( ( $total_email_opened * 100 ) / $total_email_sent ), 2 ) : 0 ;
- $click_rate = ! empty( $total_email_sent ) ? number_format_i18n( ( ( $total_email_clicked * 100 ) / $total_email_sent ), 2 ) : 0;
- $campaign['open_rate'] = $open_rate;
- $campaign['click_rate'] = $click_rate;
- $campaign['total_email_sent'] = $total_email_sent;
+ $total_email_sent = isset( $actions_count['sent'] ) ? intval( $actions_count['sent'] ) : 0;
+ $total_email_opened = isset( $actions_count['opened'] ) ? intval( $actions_count['opened'] ) : 0;
+ $total_email_clicked = isset( $actions_count['clicked'] ) ? intval( $actions_count['clicked'] ) : 0;
+ $open_rate = ! empty( $total_email_sent ) ? number_format_i18n( ( ( $total_email_opened * 100 ) / $total_email_sent ), 2 ) : 0 ;
+ $click_rate = ! empty( $total_email_sent ) ? number_format_i18n( ( ( $total_email_clicked * 100 ) / $total_email_sent ), 2 ) : 0;
+
+ $campaign = array(
+ 'open_rate' => $open_rate,
+ 'click_rate' => $click_rate,
+ 'total_email_sent' => $total_email_sent
+ );
- return $campaign;
- }
+ return $campaign;
+ }
- /**
- * Save onboarding step to WordPress options
- *
- * @param array $data
- * @return array
- */
- public static function save_onboarding_step( $data = array() ) {
+ /**
+ * Save onboarding step to WordPress options
+ *
+ * @param array $data
+ * @return array
+ */
+ public static function save_onboarding_step( $data = array() ) {
if ( is_string( $data ) ) {
$decoded_data = json_decode( $data, true );
if ( $decoded_data ) {
@@ -466,50 +507,48 @@
}
/**
- * Get all onboarding steps from WordPress options
- *
- * @return array
- */
- public static function get_onboarding_steps() {
- $steps = array(
- 'sendFirstCampaign' => 'sendFirstCampaign',
- 'importContacts' => 'importContacts',
- 'createWorkflow' => 'createWorkflow',
- 'createSubscriptionForm' => 'createSubscriptionForm'
- );
-
+ * Get onboarding steps status - reuses data from dashboard to avoid duplicate queries
+ *
+ * @param array|null $campaigns Optional campaigns array from dashboard
+ * @param array|null $forms Optional forms array from dashboard
+ * @param int|null $workflows_count Optional workflows count
+ * @return array Onboarding steps data
+ */
+ public static function get_onboarding_steps( $campaigns = null, $forms = null, $workflows_count = null ) {
+ if ( is_null( $campaigns ) ) {
$campaign_args = array(
'status' => array(
IG_ES_CAMPAIGN_STATUS_IN_ACTIVE,
IG_ES_CAMPAIGN_STATUS_ACTIVE,
),
'order_by_column' => 'ID',
- 'limit' => '5',
+ 'limit' => '1',
'order' => 'DESC',
);
$campaigns = ES()->campaigns_db->get_campaigns( $campaign_args );
+ }
+ if ( is_null( $forms ) ) {
$forms_args = array(
'order_by_column' => 'ID',
- 'limit' => '5',
+ 'limit' => '1',
'order' => 'DESC',
);
$forms = ES()->forms_db->get_forms( $forms_args );
+ }
- $workflows = ES()->workflows_db->get_workflows();
- $imported_contacts_count = ES()->contacts_db->get_contacts_count_by_source( 'import' );
-
- $onboarding_data = array(
- 'sendFirstCampaign' => ! empty( $campaigns ),
- 'importContacts' => $imported_contacts_count > 0,
- 'createSubscriptionForm' => ! empty( $forms ),
- 'createWorkflow' => ! empty( $workflows )
- );
+ if ( is_null( $workflows_count ) ) {
+ $workflows_count = ES()->workflows_db->get_workflows_count();
+ }
+
+ $imported_contacts_count = ES()->contacts_db->get_contacts_count_by_source( 'import' );
- return array(
- 'success' => true,
- 'data' => $onboarding_data
- );
+ return array(
+ 'sendFirstCampaign' => ! empty( $campaigns ),
+ 'importContacts' => $imported_contacts_count > 0,
+ 'createSubscriptionForm' => ! empty( $forms ),
+ 'createWorkflow' => $workflows_count > 0
+ );
}
@@ -564,99 +603,99 @@
}
/**
- * Get all dashboard data in a single batched request
- * Combines: lists, country_stats, audience_growth, audience_health, subscribers_stats
+ * Get contact/audience dashboard batch data in a single request
+ * Used by Audience page - combines lists, stats, growth, health, and country data
*
- * @param array $args Arguments containing list_id, days, growth_days
- * @return array Combined dashboard data
+ * @param array $data Request data containing 'days', 'list_id', 'growth_days' parameters
+ * @return array Batched audience data
*
* @since 5.9.15
*/
- public static function get_dashboard_batch_data( $args = array() ) {
-
- if ( is_string( $args ) ) {
- $decoded = json_decode( $args, true );
- if ( $decoded ) {
- $args = $decoded;
+ public static function get_contact_dashboard_batch_data( $data = array() ) {
+ if ( is_string( $data ) ) {
+ $decoded_data = json_decode( $data, true );
+ if ( $decoded_data ) {
+ $data = $decoded_data;
}
}
-
- $list_id = isset( $args['list_id'] ) ? $args['list_id'] : 'all';
- $days = isset( $args['days'] ) ? intval( $args['days'] ) : 7;
- $growth_days = isset( $args['growth_days'] ) ? intval( $args['growth_days'] ) : 30;
- $override_cache = isset( $args['override_cache'] ) ? (bool) $args['override_cache'] : false;
- // Cache key based on parameters
- $cache_key = ES_Cache::generate_key(
- 'dashboard_batch_' . $list_id . '_' . $days . '_' . $growth_days
- );
+ $days = isset( $data['days'] ) ? intval( $data['days'] ) : 7;
+ $list_id = isset( $data['list_id'] ) ? $data['list_id'] : 'all';
+ $growth_days = isset( $data['growth_days'] ) ? intval( $data['growth_days'] ) : 30;
+
+ if ( $days < 1 || $days > 365 ) {
+ $days = 7;
+ }
+
+ // 1. Get subscriber stats (audience insights)
+ $subscribers_stats = self::get_subscribers_stats( array(
+ 'list_id' => $list_id,
+ 'days' => $days,
+ ) );
+
+ // 2. Get all active lists with contact counts
+ $lists = ES_Lists_Controller::get_lists( array(
+ 'per_page' => -1,
+ ) );
+
+ // 3. Get country stats
+ $country_stats = ES_Lists_Controller::get_country_stats( array(
+ 'list_id' => $list_id,
+ 'days' => $days,
+ ) );
+
+ // 4. Get audience growth stats
+ $audience_growth = self::get_audience_growth_stats( array(
+ 'days' => $growth_days,
+ ) );
+
+ // 5. Get audience health stats
+ $audience_health = ES_Contacts_Controller::get_audience_health_stats();
- if ( ! $override_cache ) {
- $cached_data = ES_Cache::get_transient( $cache_key );
- if ( false !== $cached_data && ! empty( $cached_data ) ) {
- return $cached_data;
+ return array(
+ 'lists' => ! empty( $lists ) ? $lists : array(),
+ 'country_stats' => ! empty( $country_stats ) ? $country_stats : array(),
+ 'audience_growth' => $audience_growth,
+ 'audience_health' => $audience_health,
+ 'subscribers_stats' => $subscribers_stats,
+ );
+ }
+
+ /**
+ * Get main dashboard batch data (campaigns, forms, onboarding)
+ * Used by Main Dashboard page
+ *
+ * @param array $data Request data containing 'days' parameter
+ * @return array Combined dashboard and onboarding data
+ *
+ * @since 5.9.15
+ */
+ public static function get_main_dashboard_batch_data( $data = array() ) {
+ if ( is_string( $data ) ) {
+ $decoded_data = json_decode( $data, true );
+ if ( $decoded_data ) {
+ $data = $decoded_data;
}
- } else {
- ES_Cache::delete_transient( $cache_key );
}
-
- $response = array();
-
- try {
- // 1. Get lists data (already optimized with batch counts)
- $lists_args = array(
- 'order_by' => 'created_at',
- 'order' => 'DESC',
- 'per_page' => 20,
- 'page_number' => 1,
- );
- $lists_data = ES_Lists_Controller::get_lists( $lists_args );
-
- // 2. Get country stats (with caching)
- $country_args = array(
- 'list_id' => $list_id,
- 'days' => $days
- );
- $country_stats = ES_Lists_Controller::get_country_stats( $country_args );
-
- // 3. Get audience growth stats
- $growth_args = array(
- 'days' => $growth_days
- );
- $audience_growth = self::get_audience_growth_stats( $growth_args );
-
- // 4. Get audience health stats
- $audience_health = ES_Contacts_Controller::get_audience_health_stats( array() );
-
- // 5. Get subscribers stats
- $subscribers_args = array(
- 'list_id' => $list_id,
- 'days' => $days
- );
- $subscribers_stats = self::get_subscribers_stats( $subscribers_args );
-
- // Combine all data
- $response = array(
- 'lists' => $lists_data,
- 'country_stats' => $country_stats,
- 'audience_growth' => $audience_growth,
- 'audience_health' => $audience_health,
- 'subscribers_stats' => $subscribers_stats,
- );
-
- // Cache the result for 5 minutes (only if not overriding cache)
- if ( ! $override_cache ) {
- ES_Cache::set_transient( $cache_key, $response, 5 / 60 );
- }
-
- } catch ( Exception $e ) {
- return array(
- 'success' => false,
- 'message' => 'Failed to fetch dashboard data: ' . $e->getMessage()
- );
+
+ $days = isset( $data['days'] ) ? intval( $data['days'] ) : 7;
+
+ if ( $days < 1 || $days > 365 ) {
+ $days = 7;
}
-
- return $response;
+
+ $dashboard_data = self::get_dashboard_data( array( 'days' => $days ) );
+
+ $onboarding_steps = self::get_onboarding_steps(
+ ! empty( $dashboard_data['campaigns'] ) && is_array( $dashboard_data['campaigns'] ) ? $dashboard_data['campaigns'] : null,
+ ! empty( $dashboard_data['forms'] ) && is_array( $dashboard_data['forms'] ) ? $dashboard_data['forms'] : null,
+ null
+ );
+
+ return array(
+ 'dashboard' => $dashboard_data,
+ 'onboarding' => $onboarding_steps
+ );
}
}
--- a/email-subscribers/lite/includes/controllers/class-es-onboarding-controller.php
+++ b/email-subscribers/lite/includes/controllers/class-es-onboarding-controller.php
@@ -649,6 +649,7 @@
$notification_id = ES()->campaigns_db->insert( $notification_data );
if ( $notification_id ) {
+ ES_Campaign_Controller::add_to_new_category_format_campaign_ids($notification_id);
return array(
'status' => 'success',
'data' => array( 'notification_id' => $notification_id ),
--- a/email-subscribers/lite/includes/controllers/class-es-reports-controller.php
+++ b/email-subscribers/lite/includes/controllers/class-es-reports-controller.php
@@ -364,6 +364,71 @@
'per_page' => $notifications_args['per_page'],
);
}
+
+ public static function get_report_filter_options( $args = array() ) {
+ global $wpdb;
+
+ // Get all campaign types that have notifications sent
+ $types_query = "
+ SELECT DISTINCT c.type
+ FROM {$wpdb->prefix}ig_campaigns c
+ INNER JOIN {$wpdb->prefix}ig_mailing_queue mq ON c.id = mq.campaign_id
+ WHERE c.type IS NOT NULL AND c.type != ''
+ ORDER BY c.type
+ ";
+
+ $types = $wpdb->get_col( $types_query );
+
+ $formatted_types = array();
+ if ( ! empty( $types ) ) {
+ foreach ( $types as $type ) {
+ if ( 'newsletter' === $type ) {
+ $formatted_types[] = __( 'Broadcast', 'email-subscribers' );
+ } elseif ( 'post_notification' === $type ) {
+ $formatted_types[] = __( 'Post Notification', 'email-subscribers' );
+ } elseif ( 'post_digest' === $type ) {
+ $formatted_types[] = __( 'Post Digest', 'email-subscribers' );
+ }
+ }
+ }
+
+ $dates_query = "
+ SELECT DISTINCT
+ YEAR(start_at) as year,
+ MONTH(start_at) as month
+ FROM {$wpdb->prefix}ig_mailing_queue
+ WHERE start_at IS NOT NULL AND start_at != '0000-00-00 00:00:00'
+ ORDER BY year DESC, month DESC
+ LIMIT 24
+ ";
+
+ $dates_raw = $wpdb->get_results( $dates_query, ARRAY_A );
+
+ $dates = array();
+ $month_names = array(
+ 'January', 'February', 'March', 'April', 'May', 'June',
+ 'July', 'August', 'September', 'October', 'November', 'December'
+ );
+
+ if ( ! empty( $dates_raw ) ) {
+ foreach ( $dates_raw as $date ) {
+ $year = $date['year'];
+ $month = (int) $date['month'];
+ $key = $year . str_pad( $month, 2, '0', STR_PAD_LEFT );
+ $label = $month_names[ $month - 1 ] . ' ' . $year;
+
+ $dates[] = array(
+ 'value' => $key,
+ 'label' => $label,
+ );
+ }
+ }
+
+ return array(
+ 'types' => $formatted_types,
+ 'dates' => $dates,
+ );
+ }
public static function get_notifications( $args = array() ) {
global $wpdb, $wpbd;
@@ -434,20 +499,15 @@
}
if ( ! empty( $filter_reports_by_campaign_type ) ) {
- // Filter by campaign type - need to join with campaigns table
- // Convert display name back to database type for filtering
$type_map = array(
'Broadcast' => 'newsletter',
'Post Notification' => 'post_notification',
- 'Post Digest' => 'post_digest',
- 'Sequence' => 'sequence',
- 'Sequence Message' => 'sequence_message',
- 'Workflow' => 'workflow',
- 'Workflow Email' => 'workflow_email'
+ 'Post Digest' => 'post_digest'
);
$campaign_type = isset( $type_map[ $filter_reports_by_campaign_type ] ) ? $type_map[ $filter_reports_by_campaign_type ] : strtolower( str_replace( ' ', '_', $filter_reports_by_campaign_type ) );
+ // All campaign types use the same filter logic - subquery to campaigns table
if ( ! $add_where_clause ) {
$sql .= $wpdb->prepare( ' AND campaign_id IN (SELECT id FROM ' . IG_CAMPAIGNS_TABLE . ' WHERE type = %s)', $campaign_type );
} else {
@@ -464,7 +524,7 @@
}
}
- if ( ! $do_count_only ) {
+if ( ! $do_count_only ) {
// Prepare Order by clause
$order = ! empty( $order ) ? strtolower( $order ) : 'desc';
@@ -490,13 +550,22 @@
$sql .= ' OFFSET ' . ( $page_number - 1 ) * $per_page;
$result = $wpbd->get_results( $sql, 'ARRAY_A' );
- // Add campaign type and calculate open/click rates for each notification
if ( ! empty( $result ) && is_array( $result ) ) {
+ $campaign_ids = array_column( $result, 'campaign_id' );
+ $campaign_ids = array_filter( array_unique( $campaign_ids ) );
+
+ $all_stats = array();
+ if ( ! empty( $campaign_ids ) ) {
+ $all_stats = ES()->actions_db->get_bulk_campaign_stats(
+ $campaign_ids,
+ array( IG_MESSAGE_SENT, IG_MESSAGE_OPEN, IG_LINK_CLICK, IG_CONTACT_UNSUBSCRIBE )
+ );
+ }
+
foreach ( $result as $key => $notification ) {
$notification_id = $notification['id'];
$campaign_id = isset( $notification['campaign_id'] ) ? $notification['campaign_id'] : 0;
- // Add campaign type
if ( ! empty( $campaign_id ) ) {
$notification_type = ES()->campaigns_db->get_campaign_type_by_id( $campaign_id );
if ( ! empty( $notification_type ) ) {
@@ -507,27 +576,20 @@
$result[ $key ]['type'] = '';
}
} else {
- // No campaign_id means it's a Post Notification
$result[ $key ]['type'] = __( 'Post Notification', 'email-subscribers' );
}
- // Calculate open/click rates from actions table
- $actions_args = array(
- 'campaign_id' => $campaign_id,
- 'message_id' => $notification_id,
- 'types' => array(
- IG_MESSAGE_SENT,
- IG_MESSAGE_OPEN,
- IG_LINK_CLICK,
- IG_CONTACT_UNSUBSCRIBE
- )
+ $stats = isset( $all_stats[ $campaign_id ] ) ? $all_stats[ $campaign_id ] : array(
+ 'sent' => 0,
+ 'opened' => 0,
+ 'clicked' => 0,
+ 'unsubscribed' => 0,
);
- $actions_count = ES()->actions_db->get_actions_count( $actions_args );
- $total_sent = isset( $actions_count['sent'] ) ? $actions_count['sent'] : 0;
- $total_opened = isset( $actions_count['opened'] ) ? $actions_count['opened'] : 0;
- $total_clicked = isset( $actions_count['clicked'] ) ? $actions_count['clicked'] : 0;
- $total_unsubscribed = isset( $actions_count['unsubscribed'] ) ? $actions_count['unsubscribed'] : 0;
+ $total_sent = $stats['sent'];
+ $total_opened = $stats['opened'];
+ $total_clicked = $stats['clicked'];
+ $total_unsubscribed = $stats['unsubscribed'];
$open_rate = ! empty( $total_sent ) ? number_format_i18n( ( ( $total_opened * 100 ) / $total_sent ), 2 ) : 0;
$click_rate = ! empty( $total_sent ) ? number_format_i18n( ( ( $total_clicked * 100 ) / $total_sent ), 2 ) : 0;
--- a/email-subscribers/lite/includes/controllers/class-es-settings-controller.php
+++ b/email-subscribers/lite/includes/controllers/class-es-settings-controller.php
@@ -770,7 +770,7 @@
'ig_es_max_email_send_at_once' => get_option( 'ig_es_max_email_send_at_once', ''),
'ig_es_mailer_settings' => get_option( 'ig_es_mailer_settings', array() ),
'ig_es_ess_email' => $ess_email,
- 'ig_es_ess_branding_enabled' => get_option( 'ig_es_ess_branding_enabled', 'no' ),
+ 'ig_es_ess_branding_enabled' => get_option( 'ig_es_ess_branding_enabled', 'yes' ),
'ig_es_email_auth_headers' => self::format_email_auth_headers( get_option( 'ig_es_email_auth_headers', array() ) ),
// Gmail OAuth validation
'ig_es_gmail_valid_credentials' => self::get_gmail_valid_credentials(),
@@ -817,7 +817,7 @@
private static function set_default_settings( $options ) {
$defaults = array(
'ig_es_disable_wp_cron' => 'no',
- 'ig_es_ess_branding_enabled' => 'no',
+ 'ig_es_ess_branding_enabled' => 'yes',
'ig_es_track_email_opens' => 'no',
'ig_es_enable_ajax_form_submission' => 'no',
'ig_es_enable_welcome_email' => 'no',
@@ -848,7 +848,6 @@
'ig_es_from_name',
'ig_es_admin_emails',
'ig_es_email_type',
- 'ig_es_post_image_size',
'ig_es_track_email_opens',
'ig_es_enable_ajax_form_submission',
'ig_es_enable_welcome_email',
@@ -862,6 +861,9 @@
'ig_es_hourly_email_send_limit',
'ig_es_disable_wp_cron',
'ig_es_allow_api',
+ 'ig_es_ess_branding_enabled',
+ 'ig_es_delete_plugin_data',
+ 'ig_es_allow_tracking',
);
$textarea_fields = array(
'ig_es_unsubscribe_link_content',
@@ -888,7 +890,10 @@
$value = stripslashes_deep( $value );
- if ( in_array( $key, $text_fields, true ) ) {
+ // Special handling for image size - convert display names to slugs
+ if ( 'ig_es_post_image_size' === $key ) {
+ $value = self::convert_image_size_to_slug( $value );
+ } elseif ( in_array( $key, $text_fields, true ) ) {
$value = sanitize_text_field( $value );
} elseif ( in_array( $key, $textarea_fields, true ) ) {
$value = wp_kses_post( $value );
@@ -899,6 +904,47 @@
update_option( $key, wp_unslash( $value ), false );
}
}
+
+ /**
+ * Convert image size display name to WordPress slug
+ *
+ * @param string $value The image size value (could be display name or slug)
+ * @return string The WordPress image size slug
+ * @since 5.7.55
+ */
+ private static function convert_image_size_to_slug( $value ) {
+ // Already a valid slug, return as is
+ $valid_slugs = array( 'thumbnail', 'medium', 'medium_large', 'large', 'full', '1536x1536', '2048x2048' );
+ if ( in_array( $value, $valid_slugs, true ) ) {
+ return $value;
+ }
+
+ // Map display names to slugs for backward compatibility
+ $display_name_map = array(
+ 'Thumbnail' => 'thumbnail',
+ 'Medium' => 'medium',
+ 'Medium Size' => 'medium',
+ 'Medium large' => 'medium_large',
+ 'Large' => 'large',
+ 'Full Size' => 'full',
+ '1536×1536' => '1536x1536',
+ '2048×2048' => '2048x2048',
+ );
+
+ if ( isset( $display_name_map[ $value ] ) ) {
+ return $display_name_map[ $value ];
+ }
+
+ // Try to convert any string to a potential slug (lowercase, replace spaces/special chars with underscore)
+ $slug = strtolower( str_replace( array( ' ', '×', '-' ), array( '_', 'x', '_' ), $value ) );
+ $slug = preg_replace( '/[^a-z0-9_x]/', '', $slug );
+
+ if ( ! in_array( $slug, $valid_slugs, true ) ) {
+ $slug = 'thumbnail';
+ }
+
+ return $slug;
+ }
public static function get_registered_settings() {
--- a/email-subscribers/lite/includes/db/class-es-db-actions.php
+++ b/email-subscribers/lite/includes/db/class-es-db-actions.php
@@ -518,6 +518,78 @@
return $results;
}
+
+ public function get_bulk_campaign_stats( $campaign_ids, $types = array() ) {
+ global $wpdb;
+
+ if ( empty( $campaign_ids ) ) {
+ return array();
+ }
+
+ $campaign_ids = array_map( 'absint', $campaign_ids );
+ $campaign_ids = array_filter( $campaign_ids );
+
+ if ( empty( $campaign_ids ) ) {
+ return array();
+ }
+
+ $placeholders = implode( ',', array_fill( 0, count( $campaign_ids ), '%d' ) );
+
+ $query = "SELECT campaign_id, type, COUNT(DISTINCT contact_id) as total
+ FROM {$wpdb->prefix}ig_actions
+ WHERE campaign_id IN ($placeholders)";
+
+ if ( ! empty( $types ) ) {
+ $type_placeholders = implode( ',', array_fill( 0, count( $types ), '%d' ) );
+ $query .= " AND type IN ($type_placeholders)";
+ $query_params = array_merge( $campaign_ids, $types );
+ } else {
+ $query_params = $campaign_ids;
+ }
+
+ $query .= " GROUP BY campaign_id, type";
+
+ $results = $wpdb->get_results( $wpdb->prepare( $query, $query_params ), ARRAY_A );
+
+ $stats = array();
+ foreach ( $campaign_ids as $cid ) {
+ $stats[ $cid ] = array(
+ 'sent' => 0,
+ 'opened' => 0,
+ 'clicked' => 0,
+ 'unsubscribed' => 0,
+ );
+ }
+
+ if ( ! empty( $results ) ) {
+ foreach ( $results as $row ) {
+ $campaign_id = (int) $row['campaign_id'];
+ $type = (int) $row['type'];
+ $count = (int) $row['total'];
+
+ if ( ! isset( $stats[ $campaign_id ] ) ) {
+ continue;
+ }
+
+ switch ( $type ) {
+ case IG_MESSAGE_SENT:
+ $stats[ $campaign_id ]['sent'] = $count;
+ break;
+ case IG_MESSAGE_OPEN:
+ $stats[ $campaign_id ]['opened'] = $count;
+ break;
+ case IG_LINK_CLICK:
+ $stats[ $campaign_id ]['clicked'] = $count;
+ break;
+ case IG_CONTACT_UNSUBSCRIBE:
+ $stats[ $campaign_id ]['unsubscribed'] = $count;
+ break;
+ }
+ }
+ }
+
+ return $stats;
+ }
/**
* Build WHERE clause for actions queries
--- a/email-subscribers/lite/includes/db/class-es-db-campaigns.php
+++ b/email-subscribers/lite/includes/db/class-es-db-campaigns.php
@@ -898,21 +898,33 @@
return $updated;
}
- $id_str = '';
- $campaign_ids = esc_sql( $campaign_ids );
- if ( is_array( $campaign_ids ) && count( $campaign_ids ) > 0 ) {
- $id_str = implode( ',', $campaign_ids );
- } elseif ( is_numeric( $campaign_ids ) ) {
- $id_str = $campaign_ids;
+ $id_str = '';
+
+ $campaign_ids = array_map( 'absint', (array) $campaign_ids );
+ $campaign_ids = array_filter( $campaign_ids );
+
+ if ( empty( $campaign_ids ) ) {
+ return $updated;
}
+ $id_str = implode( ',', array_fill( 0, count( $campaign_ids ), '%d' ) );
+
if ( ! empty( $id_str ) ) {
- $query = $wpbd->prepare( "UPDATE {$wpbd->prefix}ig_campaigns SET status = %d WHERE id IN({$id_str})", $status );
- $updated = $wpbd->query( $wpbd->prepare( "UPDATE {$wpbd->prefix}ig_campaigns SET status = %d WHERE id IN({$id_str})", $status ) );
+ $updated = $wpbd->query(
+ $wpbd->prepare(
+ "UPDATE {$wpbd->prefix}ig_campaigns SET status = %d WHERE id IN({$id_str})",
+ array_merge( array( $status ), $campaign_ids )
+ )
+ );
// Changing status of child campaigns along with its parent campaign id
- $wpbd->query( $wpbd->prepare( "UPDATE {$wpbd->prefix}ig_campaigns SET status = %d WHERE parent_id IN({$id_str})", $status ) );
+ $wpbd->query(
+ $wpbd->prepare(
+ "UPDATE {$wpbd->prefix}ig_campaigns SET status = %d WHERE parent_id IN({$id_str})",
+ array_merge( array( $status ), $campaign_ids )
+ )
+ );
}
if ( $updated ) {
--- a/email-subscribers/lite/includes/db/class-es-db-contacts.php
+++ b/email-subscribers/lite/includes/db/class-es-db-contacts.php
@@ -358,15 +358,22 @@
global $wpdb;
- // Check if we have got array of list ids.
+ // Sanitize and prepare list IDs for safe SQL query
if ( is_array( $list_id ) ) {
- $list_ids_str = implode( ',', $list_id );
+ $list_id = array_map( 'absint', $list_id );
+ $placeholders = implode( ',', array_fill( 0, count( $list_id ), '%d' ) );
+ $where = $wpdb->prepare(
+ "id IN (SELECT contact_id FROM {$wpdb->prefix}ig_lists_contacts WHERE list_id IN({$placeholders}) AND status IN ('subscribed', 'confirmed'))",
+ $list_id
+ );
} else {
- $list_ids_str = $list_id;
+ $list_id = absint( $list_id );
+ $where = $wpdb->prepare(
+ "id IN (SELECT contact_id FROM {$wpdb->prefix}ig_lists_contacts WHERE list_id = %d AND status IN ('subscribed', 'confirmed'))",
+ $list_id
+ );
}
- $where = "id IN (SELECT contact_id FROM {$wpdb->prefix}ig_lists_contacts WHERE list_id IN({$list_ids_str}) AND status IN ('subscribed', 'confirmed'))";
-
return $this->get_by_conditions( $where );
}
@@ -508,14 +515,23 @@
* @since 4.3.4 Use prepare_for_in_query instead of array_to_str
*/
public function edit_contact_global_status( $ids = array(), $unsubscribed = 0 ) {
- global $wpbd;
+ global $wpdb;
- $ids_str = implode( ',', array_map( 'absint', $ids ) );
+ // Validate IDs array is not empty
+ if ( empty( $ids ) || ! is_array( $ids ) ) {
+ return false;
+ }
- return $wpbd->query(
- $wpbd->prepare(
- "UPDATE {$wpbd->prefix}ig_contacts SET unsubscribed = %d WHERE id IN({$ids_str})",
- $unsubscribed
+ // Sanitize IDs and prepare placeholders for safe SQL query
+ $ids = array_map( 'absint', $ids );
+ $placeholders = implode( ',', array_fill( 0, count( $ids ), '%d' ) );
+ $unsubscribed = absint( $unsubscribed );
+ $query_params = array_merge( array( $unsubscribed ), $ids );
+
+ return $wpdb->query(
+ $wpdb->prepare(
+ "UPDATE {$wpdb->prefix}ig_contacts SET unsubscribed = %d WHERE id IN( {$placeholders} )",
+ $query_params
)
);
}
@@ -532,7 +548,7 @@
* @since 4.3.4 Used prepare_for_in_query instead of array_to_str
*/
public function is_contact_exist_in_list( $email, $list_id ) {
- global $wpbd;
+ global $wpdb;
// Flush cache to ensure we have latest results.
ES_Cache::flush();
@@ -543,16 +559,20 @@
if ( ! empty( $contact_id ) ) {
$data['contact_id'] = $contact_id;
+ // Ensure list_id is array and sanitize
if ( ! is_array( $list_id ) ) {
$list_id = array( $list_id );
}
+ $list_id = array_map( 'absint', $list_id );
+ $placeholders = implode( ',', array_fill( 0, count( $list_id ), '%d' ) );
- $list_ids_str = implode( ',', $list_id );
+ // Merge parameters: all list IDs first, then contact_id
+ $query_params = array_merge( $list_id, array( absint( $contact_id ) ) );
- $list_contact_count = $wpbd->get_var(
- $wpbd->prepare(
- "SELECT count(*) as count FROM {$wpbd->prefix}ig_lists_contacts WHERE list_id IN ($list_ids_str) AND contact_id = %d",
- $contact_id
+ $list_contact_count = $wpdb->get_var(
+ $wpdb->prepare(
+ "SELECT count(*) as count FROM {$wpdb->prefix}ig_lists_contacts WHERE list_id IN ($placeholders) AND contact_id = %d",
+ $query_params
)
);
@@ -904,14 +924,14 @@
* @since 4.4.2
*/
public function get_total_subscribed_contacts_by_date( $days = 60 ) {
- global $wpbd;
+ global $wpdb;
$columns = array( 'DATE(created_at) as date', 'count(DISTINCT(id)) as total' );
$where = 'unsubscribed = %d';
- $args[] = 0;
+ $args = array( 0 );
if ( 0 != $days ) {
- $days = esc_sql( $days );
+ $days = absint( $days );
$where .= ' AND created_at >= DATE_SUB(NOW(), INTERVAL %d DAY)';
$args[] = $days;
}
@@ -920,7 +940,7 @@
$where .= $group_by;
- $where = $wpbd->prepare( $where, $args );
+ $where = $wpdb->prepare( $where, $args );
$results = $this->get_columns_by_condition( $columns, $where );
@@ -944,19 +964,19 @@
* @since 4.4.2
*/
public function get_total_subscribed_contacts_before_days( $days = 60 ) {
- global $wpbd;
+ global $wpdb;
$columns = array( 'count(DISTINCT(id)) as total' );
$where = 'unsubscribed = %d';
- $args[] = 0;
+ $args = array( 0 );
if ( 0 != $days ) {
- $days = esc_sql( $days );
+ $days = absint( $days );
$where .= ' AND created_at < DATE_SUB(NOW(), INTERVAL %d DAY)';
$args[] = $days;
}
- $where = $wpbd->prepare( $where, $args );
+ $where = $wpdb->prepare( $where, $args );
$results = $this->get_columns_by_condition( $columns, $where );
@@ -976,20 +996,20 @@
* @since 4.4.2
*/
public function get_total_subscribed_contacts_between_days( $days = 60 ) {
- global $wpbd;
+ global $wpdb;
$columns = array( 'count(DISTINCT(id)) as total' );
$where = 'unsubscribed = %d';
- $args[] = 0;
+ $args = array( 0 );
if ( 0 != $days ) {
- $days = esc_sql( $days );
+ $days = absint( $days );
$where .= ' AND created_at > DATE_SUB(NOW(), INTERVAL %d DAY) AND created_at < DATE_SUB(NOW(), INTERVAL %d DAY) ';
$args[] = $days * 2;
$args[] = $days;
}
- $where = $wpbd->prepare( $where, $args );
+ $where = $wpdb->prepare( $where, $args );
$results = $this->get_columns_by_condition( $columns, $where );
@@ -1030,6 +1050,32 @@
}
+ public function get_subscriber_counts_by_form_ids( $form_ids = array() ) {
+ global $wpdb;
+
+ if ( empty( $form_ids ) || ! is_array( $form_ids ) ) {
+ return array();
+ }
+
+ $form_ids = array_map( 'intval', $form_ids );
+ $placeholders = implode( ',', array_fill( 0, count( $form_ids ), '%d' ) );
+
+ $results = $wpdb->get_results(
+ $wpdb->prepare(
+ "SELECT form_id, COUNT(DISTINCT id) as total FROM {$wpdb->prefix}ig_contacts WHERE form_id IN ($placeholders) AND unsubscribed = 0 GROUP BY form_id",
+ $form_ids
+ ),
+ ARRAY_A
+ );
+
+ $counts = array();
+ foreach ( $results as $row ) {
+ $counts[ $row['form_id'] ] = intval( $row['total'] );
+ }
+
+ return $counts;
+ }
+
/**
* Migrate Ip address of subscribers from lists_contacts to contacts table
*
@@ -1411,7 +1457,7 @@
if ( ES_Cache::is_exists( $cache_key, 'query' ) ) {
return ES_Cache::get( $cache_key, 'query' );
}
-
+
$sql = "SELECT COUNT(DISTINCT c.id) FROM {$this->table_name} c";
$where_clause = '';
@@ -1504,6 +1550,26 @@
$where_conditions[] = $wpdb->prepare( "{$field} LIKE %s", '%' . $value . '%' );
break;
+ case 'does_not_contain':
+ $where_conditions[] = $wpdb->prepare( "{$field} NOT LIKE %s", '%' . $value . '%' );
+ break;
+
+ case 'starts_with':
+ $where_conditions[] = $wpdb->prepare( "{$field} LIKE %s", $value . '%' );
+ break;
+
+ case 'ends_with':
+ $where_conditions[] = $wpdb->prepare( "{$field} LIKE %s", '%' . $value );
+ break;
+
+ case 'is_greater_than':
+ $where_conditions[] = $wpdb->prepare( "{$field} > %s", $value );
+ break;
+
+ case 'is_less_than':
+ $where_conditions[] = $wpdb->prepare( "{$field} < %s", $value );
+ break;
+
case 'in':
if ( is_array( $value ) ) {
$placeholders = implode( ',', array_fill( 0, count( $value ), '%s' ) );
@@ -1514,13 +1580,13 @@
}
if ( empty( $where_conditions ) ) {
- return array();
+ return array();
}
// Preserve controller behavior: combine conditions using OR
$where_sql = implode( ' OR ', $where_conditions );
$sql = "SELECT id FROM {$contacts_table} WHERE {$where_sql}";
-
+
$results = $wpdb->get_col( $sql );
return $results ? array_map( 'intval', $results ) : array();
--- a/email-subscribers/lite/includes/db/class-es-db-lists-contacts.php
+++ b/email-subscribers/lite/includes/db/class-es-db-lists-contacts.php
@@ -324,53 +324,109 @@
*/
public function update_contact_lists( $contact_id = 0, $lists = array() ) {
- if ( empty( $contact_id ) || empty( $lists ) ) {
+ if ( empty( $contact_id ) ) {
return false;
}
- $contact_id = esc_sql( $contact_id );
- $lists = esc_sql( $lists );
+ $contact_id = absint( $contact_id );
- if ( ! empty( $lists ) ) {
-
- $optin_type_option = get_option( 'ig_es_optin_type', true );
+ $existing_lists = $this->get_list_statuses_by_contact_id( $contact_id );
+ $existing_map = array();
+ foreach ( $existing_lists as $existing ) {
+ $existing_map[ intval( $existing['list_id'] ) ] = $existing;
+ }
- $optin_type = 1;
- if ( in_array( $optin_type_option, array( 'double_opt_in', 'double_optin' ) ) ) {
- $optin_type = 2;
+ // If lists is empty, contact is removed from all lists
+ if ( empty( $lists ) ) {
+ $this->remove_contacts_from_lists( $contact_id );
+ ES()->contacts_db->invalidate_query_cache();
+ return true;
+ }
+
+ // Sanitize and validate lists input
+ $sanitized_lists = array();
+ foreach ( $lists as $list_id => $status ) {
+ $list_id = absint( $list_id );
+ $status = sanitize_text_field( $status );
+ if ( $list_id > 0 && ! empty( $status ) ) {
+ $sanitized_lists[ $list_id ] = $status;
}
+ }
+
+ // Determine which lists to remove
+ $new_list_ids = array_keys( $sanitized_lists );
+ $existing_list_ids = array_keys( $existing_map );
+ $lists_to_remove = array_diff( $existing_list_ids, $new_list_ids );
+
+ // Remove contact from lists that are no longer assigned
+ if ( ! empty( $lists_to_remove ) ) {
+ $this->remove_contacts_from_lists( $contact_id, $lists_to_remove );
+ }
- // Remove from all lists
- $this->remove_contacts_from_lists( $contact_id );
+ $optin_type_option = get_option( 'ig_es_optin_type', true );
- $data = array();
- $key = 0;
- $list_ids_to_clear = array();
- foreach ( $lists as $list_id => $status ) {
- if ( ! empty( $status ) ) {
- $data[ $key ]['list_id'] = $list_id;
- $data[ $key ]['contact_id'] = $contact_id;
- $data[ $key ]['status'] = $status;
- $data[ $key ]['optin_type'] = $optin_type;
- $data[ $key ]['subscribed_at'] = ig_get_current_date_time();
+ $optin_type = 1;
+ if ( in_array( $optin_type_option, array( 'double_opt_in', 'double_optin' ) ) ) {
+ $optin_type = 2;
+ }
- $list_ids_to_clear[] = $list_id;
- $key ++;
+ $data = array();
+ $key = 0;
+ $list_ids_to_clear = array();
+
+ foreach ( $sanitized_lists as $list_id => $status ) {
+ // Check if contact already exists in this list
+ if ( isset( $existing_map[ $list_id ] ) ) {
+ // Update existing entry - preserve subscribed_at to prevent sequence resending
+ global $wpdb;
+ $update_data = array(
+ 'status' => $status,
+ 'optin_type' => $optin_type,
+ );
+
+ $update_formats = array( '%s', '%d' );
+
+ // Only update subscribed_at if status is changing TO subscribed from another status
+ if ( 'subscribed' === $status && 'subscribed' !== $existing_map[ $list_id ]['status'] ) {
+ $update_data['subscribed_at'] = ig_get_current_date_time();
+ $update_formats[] = '%s';
}
+
+ $wpdb->update(
+ $this->table_name,
+ $update_data,
+ array(
+ 'contact_id' => $contact_id,
+ 'list_id' => $list_id,
+ ),
+ $update_formats,
+ array( '%d', '%d' )
+ );
+ } else {
+ // New entry - insert with current timestamp
+ $data[ $key ]['list_id'] = $list_id;
+ $data[ $key ]['contact_id'] = $contact_id;
+ $data[ $key ]['status'] = $status;
+ $data[ $key ]['optin_type'] = $optin_type;
+ $data[ $key ]['subscribed_at'] = ig_get_current_date_time();
+ $key ++;
}
- $result = ES()->lists_contacts_db->bulk_insert( $data );
+ $list_ids_to_clear[] = $list_id;
+ }
- if ( ! empty( $list_ids_to_clear ) ) {
- $this->clear_list_counts_cache( $list_ids_to_clear );
- }
-
- ES()->contacts_db->invalidate_query_cache();
+ $result = true;
+ if ( ! empty( $data ) ) {
+ $result = ES()->lists_contacts_db->bulk_insert( $data );
+ }
- return $result;
+ if ( ! empty( $list_ids_to_clear ) ) {
+ $this->clear_list_counts_cache( $list_ids_to_clear );
}
+
+ ES()->contacts_db->invalidate_query_cache();
- return false;
+ return $result;
}
@@ -1087,7 +1143,7 @@
$contact_id = intval( $contact_id );
- $sql = "SELECT lc.list_id, l.name as list_name, lc.status
+ $sql = "SELECT lc.list_id, l.name as list_name, lc.status, lc.subscribed_at, lc.optin_type
FROM {$this->table_name} lc
LEFT JOIN " . IG_LISTS_TABLE . " l ON lc.list_id = l.id
WHERE lc.contact_id = %d";
@@ -1361,6 +1417,66 @@
$prepared_sql = call_user_func_array( array( $wpdb, 'prepare' ), $params );
$ids = $wpdb->get_col( $prepared_sql );
+ $result = array_values( array_map( 'intval', array_filter( (array) $ids, 'is_numeric' ) ) );
+
+ ES_Cache::set( $cache_key, $result, 'query' );
+
+ return $result;
+ }
+
+ /**
+ * Get contact IDs by list with operator support
+ *
+ * @param array $list_values List IDs to filter by
+ * @param string $operator Operator to apply ('is equal to', 'is not equal to', etc.)
+ * @return array Contact IDs
+ *
+ * @since 5.9.15
+ */
+ public function get_contact_ids_by_list_operator( $list_values = array(), $operator = 'is equal to' ) {
+ global $wpdb;
+
+ if ( empty( $list_values ) ) {
+ return array();
+ }
+
+ $cache_key = ES_Cache::generate_key( 'contact_ids_by_list_op_' . md5( serialize( array( $list_values, $operator ) ) ) );
+
+ if ( ES_Cache::is_exists( $cache_key, 'query' ) ) {
+ return ES_Cache::get( $cache_key, 'query' );
+ }
+
+ $list_values = array_map( 'intval', $list_values );
+ $list_count = count( $list_values );
+
+ $sql = "SELECT DISTINCT contact_id FROM {$this->table_name} WHERE 1=1";
+
+ switch ( $operator ) {
+ case 'is equal to':
+ case 'contains':
+ // Contact is in any of these lists
+ $placeholders = implode( ', ', array_fill( 0, $list_count, '%d' ) );
+ $sql .= " AND list_id IN ($placeholders)";
+ break;
+
+ case 'is not equal to':
+ case 'does not contain':
+ // Contact is NOT in any of these lists
+ $placeholders = implode( ', ', array_fill( 0, $list_count, '%d' ) );
+ $sql .= " AND list_id NOT IN ($placeholders)";
+ break;
+
+ default:
+ // Default to 'is equal to'
+ $placeholders = implode( ', ', array_fill( 0, $list_count, '%d' ) );
+ $sql .= " AND list_id IN ($placeholders)";
+ break;
+ }
+
+ $params = array_merge( array( $sql ), $list_values );
+ $prepared_sql = call_user_func_array( array( $wpdb, 'prepare' ), $params );
+ $ids = $wpdb->get_col( $prepared_sql );
+
$result = array_values( array_map( 'intval', array_filter( (array) $ids, 'is_numeric' ) ) );
ES_Cache::set( $cache_key, $result, 'query' );
--- a/email-subscribers/lite/includes/db/class-es-db-lists.php
+++ b/email-subscribers/lite/includes/db/class-es-db-lists.php
@@ -92,8 +92,32 @@
*
* @since 4.0.0
*/
- public function get_lists() {
- return $this->get_all();
+ public function get_lists( $args = array() ) {
+ if ( empty( $args ) ) {
+ return $this->get_all();
+ }
+
+ $allowed_columns = array( 'id', 'name', 'created_at', 'updated_at', 'slug', 'status' );
+ $order_by = ! empty( $args['order_by'] ) ? esc_sql( $args['order_by'] ) : 'id';
+
+ if ( ! in_array( $order_by, $allowed_columns, true ) ) {
+ $order_by = 'id';
+ }
+
+ $order = ! empty( $args['order'] ) ? esc_sql( strtoupper( $args['order'] ) ) : 'ASC';
+
+ if ( ! in_array( $order, array( 'ASC', 'DESC' ), true ) ) {
+ $order = 'ASC';
+ }
+
+ global $wpdb;
+ $query = "SELECT * FROM {$this->table_name} ORDER BY {$order_by} {$order}";
+
+ if ( ! empty( $args['limit'] ) ) {
+ $query .= $wpdb->prepare( ' LIMIT %d', intval( $args['limit'] ) );
+ }
+
+ return $wpdb->get_results( $query, ARRAY_A );
}
/**
--- a/email-subscribers/lite/includes/workflows/db/class-es-db-workflows.php
+++ b/email-subscribers/lite/includes/workflows/db/class-es-db-workflows.php
@@ -117,6 +117,11 @@
*
* @since 4.4.1
*/
+ public function get_workflows_count() {
+ global $wpdb;
+ return (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}ig_workflows" );
+ }
+
public function get_workflows( $query_args = array(), $output = ARRAY_A, $do_count_only = false ) {
global $wpdb, $wpbd;
@@ -365,6 +370,13 @@
return $updated;
}
+ if ( is_array( $workflow_ids ) ) {
+ $workflow_ids = array_map( 'absint', $workflow_ids );
+ $workflow_ids = array_filter( $workflow_ids );
+
+ } else {
+ $workflow_ids = absint( $workflow_ids );
+ }
$workflow_ids = esc_sql( $workflow_ids );
// Variable to hold workflow ids seperated by commas.