Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/mailchimp-for-woocommerce/admin/class-mailchimp-woocommerce-admin.php
+++ b/mailchimp-for-woocommerce/admin/class-mailchimp-woocommerce-admin.php
@@ -132,13 +132,13 @@
* @since 1.0.0
*/
public function enqueue_styles( $hook ) {
- wp_enqueue_style( $this->plugin_name, plugin_dir_url( __FILE__ ) . 'css/mailchimp-woocommerce-admin.css', array(), $this->version . '.21' );
+ wp_enqueue_style( $this->plugin_name, plugin_dir_url( __FILE__ ) . 'css/mailchimp-woocommerce-admin.css', array(), $this->version );
if ( strpos( $hook, 'page_mailchimp-woocommerce' ) !== false || strpos( $hook, 'create-mailchimp-account' ) !== false) {
if ( get_bloginfo( 'version' ) < '5.3' ) {
wp_enqueue_style( $this->plugin_name . '-settings', plugin_dir_url( __FILE__ ) . 'css/mailchimp-woocommerce-admin-settings-5.2.css', array(), $this->version );
}
- wp_enqueue_style( $this->plugin_name . '-settings', plugin_dir_url( __FILE__ ) . 'css/mailchimp-woocommerce-admin-settings.css', array(), $this->version . '.01' );
+ wp_enqueue_style( $this->plugin_name . '-settings', plugin_dir_url( __FILE__ ) . 'css/mailchimp-woocommerce-admin-settings.css', array(), $this->version );
// Update v2
wp_enqueue_style( $this->plugin_name . '-settings-v2', plugin_dir_url( __FILE__ ) . 'v2/assets/css/styles.css', array(), $this->version);
// End update v2
@@ -573,6 +573,18 @@
// we have some stores that are in a perpetual state of syncing - causing issues with support.
// trying to adjust things on plugin update
//$this->fix_is_syncing_problem();
+
+ // Stores that completed a sync on older versions can still carry a
+ // stale sync.initial_sync flag, which makes every API call send
+ // X-Data-Mode: historical — live orders get treated as historical
+ // data. Only safe to clear when no sync is running; an in-flight
+ // initial sync legitimately needs the flag.
+ if ( ! (bool) mailchimp_get_data( 'sync.syncing' ) ) {
+ Mailchimp_Woocommerce_DB_Helpers::delete_option( 'mailchimp-woocommerce-sync.initial_sync' );
+ // rebuild the per-request env snapshot so the rest of this
+ // request stops sending the historical header immediately.
+ mailchimp_environment_variables( true );
+ }
}
// Carts-table one-time cleanup: add PRIMARY KEY on email column and
--- a/mailchimp-for-woocommerce/admin/v2/templates/connect-accounts/button-actions.php
+++ b/mailchimp-for-woocommerce/admin/v2/templates/connect-accounts/button-actions.php
@@ -14,18 +14,19 @@
<span><?php esc_html_e( 'Connect your store to Mailchimp', 'mailchimp-for-woocommerce' ); ?></span>
</legend>
<div class="mc-wc-actions">
- <a id="mailchimp-oauth-connect" class="mc-wc-btn mc-wc-btn-primary oauth-connect js-mailchimp-woocommerce-send-event" data-mc-event="click_connect_account"><?php esc_html_e( 'Connect Account', 'mailchimp-for-woocommerce' ); ?></a>
+ <a id="mailchimp-oauth-connect" class="mc-wc-btn mc-wc-btn-primary oauth-connect js-mailchimp-woocommerce-send-event" data-mc-event="click_connect_account">
+ <?php esc_html_e( 'Connect Account', 'mailchimp-for-woocommerce' ); ?>
+ <img class="sync-loader" style="display: none;" src="<?php echo esc_attr( plugin_dir_url( __FILE__ ) . '../../assets/images/3dotpurple.gif' ); ?>"/>
+ </a>
<?php if (!isset($promo_active) || !$promo_active): ?>
<a class="mc-wc-btn mc-wc-btn-primary-outline create-account js-mailchimp-woocommerce-send-event" data-mc-event="click_create_account" href='<?php echo esc_url($create_account_url) ?>'><?php esc_html_e( 'Create account', 'mailchimp-for-woocommerce' ); ?></a>
<?php endif; ?>
</div>
<input type="hidden" id="<?php echo esc_attr( $this->plugin_name ); ?>-mailchimp-api-key" name="<?php echo esc_attr( $this->plugin_name ); ?>[mailchimp_api_key]" value="<?php echo isset( $options['mailchimp_api_key'] ) ? esc_attr( $options['mailchimp_api_key'] ) : ''; ?>" required/>
- <?php if ($show_connection_messages) : ?>
<p id="mailchimp-oauth-waiting" class="oauth-description"><?php esc_html_e( 'Connecting. A new window will open with Mailchimp's OAuth service. Please log-in and we will take care of the rest.', 'mailchimp-for-woocommerce' ); ?></p>
- <p id="mailchimp-oauth-error" class="oauth-description"><?php esc_html_e( 'Error, can't login.', 'mailchimp-for-woocommerce' ); ?></p>
+ <p id="mailchimp-oauth-error" class="oauth-description"><?php esc_html_e( 'Login failed.', 'mailchimp-for-woocommerce' ); ?></p>
<p id="mailchimp-oauth-connecting" class="oauth-description"><?php esc_html_e( 'Connection in progress', 'mailchimp-for-woocommerce' ); ?><span class="spinner" style="visibility:visible; margin: 0 10px;"></span></p>
<p id="mailchimp-oauth-connected" class="oauth-description "><?php esc_html_e( 'Connected! Please wait while loading next step', 'mailchimp-for-woocommerce' ); ?></p>
- <?php endif; ?>
</fieldset>
<?php include_once 'create-account-popup.php'; ?>
No newline at end of file
--- a/mailchimp-for-woocommerce/blocks/build/newsletter-block-frontend.asset.php
+++ b/mailchimp-for-woocommerce/blocks/build/newsletter-block-frontend.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('react-jsx-runtime', 'wc-blocks-checkout', 'wc-blocks-shared-hocs', 'wc-settings', 'wp-element', 'wp-i18n', 'wp-polyfill'), 'version' => 'ff2088c6ddfed08ef5c9');
+<?php return array('dependencies' => array('react', 'wc-blocks-checkout', 'wc-blocks-shared-hocs', 'wc-settings', 'wp-element', 'wp-i18n', 'wp-polyfill'), 'version' => '7aa258ddc4b395ba31fd');
--- a/mailchimp-for-woocommerce/blocks/build/newsletter-block.asset.php
+++ b/mailchimp-for-woocommerce/blocks/build/newsletter-block.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('react-jsx-runtime', 'wc-blocks-checkout', 'wc-settings', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-i18n', 'wp-polyfill'), 'version' => '14538527c4c5fa4c3b49');
+<?php return array('dependencies' => array('react', 'wc-blocks-checkout', 'wc-settings', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-i18n', 'wp-polyfill'), 'version' => 'bfdf2a3d103adddd0f2c');
--- a/mailchimp-for-woocommerce/blocks/build/pixel-tracking.asset.php
+++ b/mailchimp-for-woocommerce/blocks/build/pixel-tracking.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('wp-hooks', 'wp-polyfill'), 'version' => '740c58b265909004380b');
+<?php return array('dependencies' => array('wp-hooks', 'wp-polyfill'), 'version' => 'e1e1f7312242942b6f0e');
--- a/mailchimp-for-woocommerce/blocks/build/sms-consent-block-frontend.asset.php
+++ b/mailchimp-for-woocommerce/blocks/build/sms-consent-block-frontend.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('react', 'react-jsx-runtime', 'wc-blocks-checkout', 'wc-blocks-shared-hocs', 'wc-settings', 'wp-data', 'wp-element', 'wp-i18n', 'wp-polyfill'), 'version' => 'ee58495082851a6e6b5d');
+<?php return array('dependencies' => array('react', 'wc-blocks-checkout', 'wc-blocks-shared-hocs', 'wc-settings', 'wp-data', 'wp-element', 'wp-i18n', 'wp-polyfill'), 'version' => 'ae1ebb1bb55549e6acb5');
--- a/mailchimp-for-woocommerce/blocks/build/sms-consent-block.asset.php
+++ b/mailchimp-for-woocommerce/blocks/build/sms-consent-block.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('react-jsx-runtime', 'wc-blocks-checkout', 'wc-settings', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-element', 'wp-i18n', 'wp-polyfill'), 'version' => 'fd6ce3f1960c8fb4b18f');
+<?php return array('dependencies' => array('react', 'wc-blocks-checkout', 'wc-settings', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-element', 'wp-i18n', 'wp-polyfill'), 'version' => '4d5808f4ac0241b07c91');
--- a/mailchimp-for-woocommerce/blocks/newsletter.php
+++ b/mailchimp-for-woocommerce/blocks/newsletter.php
@@ -15,25 +15,34 @@
add_action( 'woocommerce_store_api_checkout_update_order_from_request', array( 'Mailchimp_Woocommerce_Newsletter_Blocks_Integration', 'order_processed' ), 10, 2 );
add_action( 'woocommerce_store_api_checkout_order_processed', array( 'Mailchimp_Woocommerce_Newsletter_Blocks_Integration', 'order_customer_processed' ) );
- if (mailchimp_sms_consent_enabled()) {
- add_action( 'woocommerce_store_api_checkout_update_order_from_request', array( 'Mailchimp_Woocommerce_Sms_Blocks_Integration', 'order_processed' ), 10, 2 );
- add_action( 'woocommerce_store_api_checkout_order_processed', array( 'Mailchimp_Woocommerce_Sms_Blocks_Integration', 'order_customer_processed' ) );
- }
require_once dirname( __FILE__ ) . '/woocommerce-blocks-integration.php';
- require_once dirname( __FILE__ ) . '/woocommerce-sms-blocks-integration.php';
- require_once dirname( __FILE__ ) . '/woocommerce-blocks-extend-store-endpoint.php';
- require_once dirname( __FILE__ ) . '/woocommerce-blocks-extend-store-endpoint-sms.php';
+ require_once dirname( __FILE__ ) . '/woocommerce-blocks-extend-store-endpoint.php';
add_action(
'woocommerce_blocks_checkout_block_registration',
function( $integration_registry ) {
$integration_registry->register( new Mailchimp_Woocommerce_Newsletter_Blocks_Integration() );
- $integration_registry->register( new Mailchimp_Woocommerce_Sms_Blocks_Integration() );
}
);
Mailchimp_Woocommerce_Newsletter_Blocks_Extend_Store_Endpoint::init();
- Mailchimp_Woocommerce_Sms_Blocks_Extend_Store_Endpoint::init();
+
+ if (mailchimp_sms_consent_active()) {
+ add_action( 'woocommerce_store_api_checkout_update_order_from_request', array( 'Mailchimp_Woocommerce_Sms_Blocks_Integration', 'order_processed' ), 10, 2 );
+ add_action( 'woocommerce_store_api_checkout_order_processed', array( 'Mailchimp_Woocommerce_Sms_Blocks_Integration', 'order_customer_processed' ) );
+
+ require_once dirname( __FILE__ ) . '/woocommerce-sms-blocks-integration.php';
+ require_once dirname( __FILE__ ) . '/woocommerce-blocks-extend-store-endpoint-sms.php';
+
+ add_action(
+ 'woocommerce_blocks_checkout_block_registration',
+ function( $integration_registry ) {
+ $integration_registry->register( new Mailchimp_Woocommerce_Sms_Blocks_Integration() );
+ }
+ );
+
+ Mailchimp_Woocommerce_Sms_Blocks_Extend_Store_Endpoint::init();
+ }
}
} );
No newline at end of file
--- a/mailchimp-for-woocommerce/bootstrap.php
+++ b/mailchimp-for-woocommerce/bootstrap.php
@@ -135,7 +135,7 @@
$cached = (object) array(
'repo' => 'master',
'environment' => 'production', // staging or production
- 'version' => '6.1.1',
+ 'version' => '6.2',
'php_version' => phpversion(),
'wp_version' => (empty($wp_version) ? 'Unknown' : $wp_version),
'wc_version' => function_exists('WC') ? WC()->version : null,
@@ -1596,7 +1596,7 @@
*/
function mailchimp_get_wc_customer($email) {
global $wpdb;
- return $wpdb->get_row( "SELECT * FROM `{$wpdb->prefix}wc_customer_lookup` WHERE `email` = '{$email}'" );
+ return $wpdb->get_row( $wpdb->prepare( "SELECT * FROM `{$wpdb->prefix}wc_customer_lookup` WHERE `email` = %s", $email ) );
}
/**
@@ -2048,7 +2048,8 @@
*
* @return bool
*/
-function mailchimp_sms_consent_enabled() {
+function mailchimp_sms_consent_active()
+{
if (!MailChimp_Sms_Consent::isEligibleCountry()) {
return false;
}
@@ -2059,14 +2060,24 @@
return false;
}
- // Classic checkout path — admin toggled the option on.
+ return true;
+}
+
+function mailchimp_sms_consent_enabled_in_classic_checkout()
+{
$options = mailchimp_get_admin_options();
if (!empty($options['mailchimp_sms_consent_enabled'])) {
return true;
}
+ return false;
+}
+
+function mailchimp_sms_consent_enabled() {
+ // Classic checkout path — admin toggled the option on.
+
// Block checkout path — check the block's "usingSmsConsent" attribute.
- return mailchimp_sms_block_enabled_in_checkout();
+ return mailchimp_sms_consent_active() && (mailchimp_sms_block_enabled_in_checkout() || mailchimp_sms_consent_enabled_in_classic_checkout());
}
/**
--- a/mailchimp-for-woocommerce/includes/api/class-mailchimp-api.php
+++ b/mailchimp-for-woocommerce/includes/api/class-mailchimp-api.php
@@ -11,6 +11,7 @@
protected $auth_type = 'key';
protected $allow_audience_put = true;
protected $auto_doi = false;
+ protected $is_syncing = null;
/** @var null|MailChimp_WooCommerce_MailChimpApi */
protected static $instance = null;
@@ -42,6 +43,17 @@
}
}
+ public function setIsSyncing($bool = true)
+ {
+ $this->is_syncing = (bool) $bool;
+ return $this;
+ }
+
+ public function isSyncing()
+ {
+ return $this->is_syncing;
+ }
+
/**
* @param $bool
* @return $this
@@ -68,7 +80,7 @@
* @return $this
*/
public function setApiKey( $key ) {
- $parts = str_getcsv( $key, '-' );
+ $parts = str_getcsv( $key, '-' , '"', '');
if ( count( $parts ) == 2 ) {
$this->data_center = $parts[1];
@@ -720,7 +732,9 @@
'sms_phone' => $sms_phone,
'marketing_consent' => array(
'status' => $subscribed ? 'confirmed' : 'pending',
- 'source' => 'Mailchimp for Woocommerce',
+ 'source' => array(
+ 'name' => 'Mailchimp for Woocommerce',
+ ),
),
);
@@ -756,7 +770,9 @@
'email' => $email,
'marketing_consent' => [
'status' => $email_subscribed ? 'confirmed' : 'unknown',
- 'source' => 'Mailchimp for Woocommerce',
+ 'source' => array(
+ 'name' => 'Mailchimp for Woocommerce',
+ ),
]
],
'sms_channel' => $sms_channel,
@@ -816,7 +832,9 @@
'sms_phone' => $sms_phone,
'marketing_consent' => array(
'status' => $subscribed ? 'confirmed' : 'unsubscribed',
- 'source' => 'Mailchimp for Woocommerce',
+ 'source' => array(
+ 'name' => 'Mailchimp for Woocommerce',
+ ),
),
);
@@ -1024,7 +1042,7 @@
// Try to get SMS settings for the audience
$result = $this->get( "lists/{$list_id}/sms-program" );
- if ( isset( $result['sms_enabled'] ) && $result['sms_enabled'] ) {
+ if (!empty($result['sms_program'][0]['can_send'])) {
return array(
'enabled' => true,
'sending_countries' => isset( $result['sending_countries'] ) ? $result['sending_countries'] : array(),
@@ -1047,10 +1065,10 @@
*/
public function getCachedSmsApplicationStatus( $list_id ) {
$transient_key = "mailchimp_sms_status_{$list_id}";
- $cached = mailchimp_get_transient( $transient_key );
+ $cached = mailchimp_get_transient( $transient_key, false );
if ( $cached !== false ) {
- return $cached;
+ return $cached['value'];
}
try {
@@ -1062,7 +1080,6 @@
return false;
}
}
-
/**
* Check if a country is in the merchant's SMS sending countries
*
@@ -2618,6 +2635,9 @@
return $GDPRfields;
}
+ $filteredMinutes = (int) apply_filters('mailchimp_checkout_overwrite_gdpr_cache_minutes', $minutes);
+ $minutes = $filteredMinutes <= 0 ? $minutes : $filteredMinutes;
+
try {
$GDPRfields = $this->getGDPRFields( $list_id );
set_transient( $transient, $GDPRfields, 60 * $minutes );
@@ -3136,7 +3156,7 @@
$headers
);
- if ($env->initial_sync) {
+ if ($this->is_syncing) {
$headers[] = 'X-Data-Mode: historical';
}
--- a/mailchimp-for-woocommerce/includes/class-mailchimp-woocommerce-newsletter.php
+++ b/mailchimp-for-woocommerce/includes/class-mailchimp-woocommerce-newsletter.php
@@ -117,259 +117,10 @@
}
echo apply_filters( 'mailchimp_woocommerce_newsletter_field', $checkbox, $status, $label);
-
- // Render SMS consent fields after newsletter checkbox
- $this->applySmsConsentField();
- }
- }
-
- /**
- * Render SMS consent checkbox and phone field for classic checkout
- */
- public function applySmsConsentField()
- {
- // Check if SMS is enabled in settings
- if (!$this->isSmsEnabled()) {
- return;
- }
-
- // Check if merchant has approved SMS application
- if (!$this->merchantHasSmsApproved()) {
- return;
- }
-
- // Compliance: checkbox must always be unchecked by default, label and disclaimer are fixed
- $sms_label = __('Text me with news and offers', 'mailchimp-for-woocommerce');
-
- $audience_name = $this->getAudienceName();
- $prefix = !empty($audience_name) ? $audience_name . ' – ' : '';
- $in_sentence = !empty($audience_name) ? $audience_name : 'us';
- $sms_disclaimer = $prefix . __('By providing your phone number, you agree to receive promotional and marketing messages (e.g. abandoned carts), notifications, and customer service communications from '.$in_sentence.'. Message and data rates map apply. Consent is not a condition of purchase. Message frequency varies. Text HELP for help. Text STOP to cancel. See Terms and Privacy Policy.', 'mailchimp-for-woocommerce');
-
- // Always unchecked by default per compliance
- $sms_status = false;
- $sms_phone = '';
- $hide_sms_for_subscriber = false;
-
- // Check logged-in user's SMS subscription status
- if (is_user_logged_in()) {
- $user_sms_status = get_user_meta(get_current_user_id(), 'mailchimp_woocommerce_sms_subscribed', true);
- $sms_phone = get_user_meta(get_current_user_id(), 'mailchimp_woocommerce_sms_phone', true);
- $hide_sms_for_subscriber = $user_sms_status === true || $user_sms_status === '1';
-
- if ($user_sms_status === '' || $user_sms_status === null) {
- $sms_status = false;
- } else {
- $sms_status = (bool) $user_sms_status;
- }
- }
-
- // Don't show if already subscribed to SMS
- if (is_checkout() && $hide_sms_for_subscriber) {
- return;
- }
-
- // Build SMS consent HTML
- $sms_html = '<div class="mailchimp-sms-consent" style="margin-top: 15px;">';
-
- // SMS Checkbox
- $sms_html .= '<p class="form-row form-row-wide mailchimp-sms-checkbox">';
- $sms_html .= '<label for="mailchimp_woocommerce_sms_subscribe" class="woocommerce-form__label woocommerce-form__label-for-checkbox inline">';
- $sms_html .= '<input class="woocommerce-form__input woocommerce-form__input-checkbox input-checkbox" id="mailchimp_woocommerce_sms_subscribe" type="checkbox" name="mailchimp_woocommerce_sms_subscribe" value="1"' . ($sms_status ? ' checked="checked"' : '') . '> ';
- $sms_html .= '<span>' . esc_html($sms_label) . '</span></label>';
- $sms_html .= '</p>';
-
- // SMS Phone field (conditionally displayed via JS)
- $sms_html .= '<div id="mailchimp-sms-phone-wrapper" class="form-row form-row-wide from-newsletter" style="display: ' . ($sms_status ? 'block' : 'none') . '; margin-left: 28px;">';
- $sms_html .= '<label for="mailchimp_woocommerce_sms_phone">' . __('SMS Phone Number', 'mailchimp-for-woocommerce') . ' <abbr class="required" title="required">*</abbr></label>';
- $sms_html .= '<input type="tel" class="input-text" id="mailchimp_woocommerce_sms_phone" name="mailchimp_woocommerce_sms_phone" placeholder="+1 (555) 123-4567" value="' . esc_attr($sms_phone) . '">';
- $sms_html .= '<small class="mailchimp-sms-disclaimer" style="display: block; color: #666; font-size: 12px; margin-top: 8px; line-height: 1.4;">' . esc_html($sms_disclaimer) . '</small>';
- $sms_html .= '</div>';
-
- $sms_html .= '</div>';
- $sms_html .= '<div class="clear"></div>';
-
- // Get SMS sending countries for JS
- $sms_countries = $this->getSmsSendingCountries();
- $sms_countries_json = !empty($sms_countries) ? json_encode($sms_countries) : '[]';
-
- // JavaScript to toggle phone field visibility, validation, and country filtering
- $sms_html .= '<script type="text/javascript">
- jQuery(document).ready(function($) {
- var smsCheckbox = $("#mailchimp_woocommerce_sms_subscribe");
- var smsPhoneWrapper = $("#mailchimp-sms-phone-wrapper");
- var smsPhoneInput = $("#mailchimp_woocommerce_sms_phone");
- var smsConsentWrapper = $(".mailchimp-sms-consent");
- var smsSendingCountries = ' . $sms_countries_json . ';
-
- function isCountryEligible(countryCode) {
- // If no countries configured, allow all
- if (!smsSendingCountries || smsSendingCountries.length === 0) {
- return true;
- }
- return smsSendingCountries.indexOf(countryCode.toUpperCase()) !== -1;
- }
-
- function checkBillingCountry() {
- var billingCountry = $("#billing_country").val();
- if (billingCountry && !isCountryEligible(billingCountry)) {
- smsConsentWrapper.slideUp();
- smsCheckbox.prop("checked", false);
- smsPhoneInput.prop("required", false).val("");
- } else {
- smsConsentWrapper.slideDown();
- }
- }
-
- function toggleSmsPhone() {
- if (smsCheckbox.is(":checked")) {
- smsPhoneWrapper.slideDown();
- smsPhoneInput.prop("required", true);
- } else {
- smsPhoneWrapper.slideUp();
- smsPhoneInput.prop("required", false).val("");
- }
- }
-
- smsCheckbox.on("change", toggleSmsPhone);
- toggleSmsPhone();
-
- // Watch for billing country changes
- $("#billing_country").on("change", checkBillingCountry);
- $(document.body).on("updated_checkout", checkBillingCountry);
- checkBillingCountry();
-
- function mailchimpValidateSmsPhone(value) {
- console.log("validate_callback for smsPhone", { value });
-
- // 1) Type check (match PHP logic)
- if (value !== null && typeof value !== "string") {
- return {
- error: "api-error",
- message: "SMS phone must be a string"
- };
- }
- // 2) Only validate if not empty
- if (value && value.length > 0) {
- // Remove spaces, dashes, parentheses (same as preg_replace)
- const cleaned = value.replace(/[s-()]/g, "");
- // 3) Same regex as PHP
- const phoneRegex = /^+?[1-9]d{6,14}$/;
- if (!phoneRegex.test(cleaned)) {
- return {
- error: "api-error",
- message: "Invalid phone number format"
- };
- }
- }
- return true;
- }
-
- // Validation on checkout
- $("form.checkout").on("checkout_place_order", function() {
-
- if (!smsCheckbox.is(":checked")) {
- console.log("no sms consent, skipping validation");
- return true;
- }
- console.log("checking sms consent", smsPhoneInput.val());
- if (!smsPhoneInput.val().trim()) {
- alert("' . esc_js(__('Please enter a valid phone number for SMS consent.', 'mailchimp-for-woocommerce')) . '");
- smsPhoneInput.focus();
- return false;
- }
-
- const result = mailchimpValidateSmsPhone(smsPhoneInput.val().trim());
- console.log("sms result", result);
- if (result !== true) {
- alert(result.message || "Invalid SMS phone number.");
- smsPhoneInput.focus();
- return false;
- }
- return true;
- });
- });
- </script>';
-
- echo apply_filters('mailchimp_woocommerce_sms_consent_field', $sms_html, $sms_status, $sms_label);
- }
-
- /**
- * Check if SMS marketing is enabled
- *
- * @return bool
- */
- public function isSmsEnabled()
- {
- return (bool) $this->getOption('mailchimp_sms_enabled', false);
- }
-
- /**
- * Check if merchant has an approved SMS application
- *
- * @return bool
- */
- public function merchantHasSmsApproved()
- {
- try {
- if (!mailchimp_is_configured()) {
- return false;
- }
- $list_id = mailchimp_get_list_id();
- if (!$list_id) {
- return false;
- }
- $api = mailchimp_get_api();
- $sms_status = $api->getCachedSmsApplicationStatus($list_id);
- return $sms_status && !empty($sms_status['enabled']);
- } catch (Exception $e) {
- return false;
- }
- }
-
- /**
- * Get SMS sending countries for the merchant
- *
- * @return array
- */
- public function getSmsSendingCountries()
- {
- try {
- if (!mailchimp_is_configured()) {
- return array();
- }
- $list_id = mailchimp_get_list_id();
- if (!$list_id) {
- return array();
- }
- $api = mailchimp_get_api();
- $sms_status = $api->getCachedSmsApplicationStatus($list_id);
- if ($sms_status && !empty($sms_status['sending_countries'])) {
- return $sms_status['sending_countries'];
- }
- return array();
- } catch (Exception $e) {
- return array();
}
}
/**
- * Check if a country is eligible for SMS
- *
- * @param string $country_code 2-letter country code
- * @return bool
- */
- public function isCountryEligibleForSms($country_code)
- {
- $sending_countries = $this->getSmsSendingCountries();
- if (empty($sending_countries)) {
- // If no countries configured, allow all (graceful fallback)
- return true;
- }
- return in_array(strtoupper($country_code), $sending_countries, true);
- }
-
- /**
* Get the audience name for disclaimer
*
* @return string
@@ -399,7 +150,6 @@
public function processNewsletterField($order_id, $posted)
{
$this->handleStatus($order_id);
- $this->handleSmsStatus($order_id);
}
/**
@@ -408,7 +158,6 @@
public function processPayPalNewsletterField($order)
{
$this->handleStatus($order->get_id());
- $this->handleSmsStatus($order->get_id());
}
/**
@@ -423,7 +172,6 @@
}
$this->handleStatus();
- $this->handleSmsStatus();
}
/**
@@ -458,97 +206,4 @@
return false;
}
-
- /**
- * Handle SMS subscription status from classic checkout
- *
- * @param null $order_id
- * @return bool
- */
- protected function handleSmsStatus($order_id = null)
- {
- // Check if SMS is enabled
- if (!$this->isSmsEnabled()) {
- return false;
- }
-
- $sms_checkbox_key = 'mailchimp_woocommerce_sms_subscribe';
- $sms_phone_key = 'mailchimp_woocommerce_sms_phone';
- $sms_subscribed_meta = 'mailchimp_woocommerce_sms_subscribed';
- $sms_phone_meta = 'mailchimp_woocommerce_sms_phone';
- $logged_in = is_user_logged_in();
-
- // Get SMS consent status from POST
- $sms_subscribed = isset($_POST[$sms_checkbox_key]) ? (bool) $_POST[$sms_checkbox_key] : false;
- $sms_phone = isset($_POST[$sms_phone_key]) ? sanitize_text_field($_POST[$sms_phone_key]) : '';
-
- // Sanitize phone number - keep only + and digits
- $sms_phone = preg_replace('/[^+d]/', '', $sms_phone);
-
- // If they didn't check the box or didn't provide a phone, don't save anything
- if (!$sms_subscribed || empty($sms_phone)) {
- return false;
- }
-
- // Update order meta
- if ($order_id) {
- MailChimp_WooCommerce_HPOS::update_order_meta($order_id, $sms_subscribed_meta, true);
- MailChimp_WooCommerce_HPOS::update_order_meta($order_id, $sms_phone_meta, $sms_phone);
- }
-
- // Update user meta if logged in
- if ($logged_in) {
- update_user_meta(get_current_user_id(), $sms_subscribed_meta, true);
- update_user_meta(get_current_user_id(), $sms_phone_meta, $sms_phone);
- }
-
- return true;
- }
-
- /**
- * Get SMS subscription data from order
- *
- * @param int $order_id
- * @return array|false
- */
- public static function getSmsDataFromOrder($order_id)
- {
- $wc_order = wc_get_order($order_id);
- if (!$wc_order) {
- return false;
- }
-
- $sms_subscribed = $wc_order->get_meta('mailchimp_woocommerce_sms_subscribed');
- $sms_phone = $wc_order->get_meta('mailchimp_woocommerce_sms_phone');
-
- if (!$sms_subscribed || empty($sms_phone)) {
- return false;
- }
-
- return array(
- 'subscribed' => (bool) $sms_subscribed,
- 'phone' => $sms_phone,
- );
- }
-
- /**
- * Get SMS subscription data from user
- *
- * @param int $user_id
- * @return array|false
- */
- public static function getSmsDataFromUser($user_id)
- {
- $sms_subscribed = get_user_meta($user_id, 'mailchimp_woocommerce_sms_subscribed', true);
- $sms_phone = get_user_meta($user_id, 'mailchimp_woocommerce_sms_phone', true);
-
- if (!$sms_subscribed || empty($sms_phone)) {
- return false;
- }
-
- return array(
- 'subscribed' => (bool) $sms_subscribed,
- 'phone' => $sms_phone,
- );
- }
}
--- a/mailchimp-for-woocommerce/includes/class-mailchimp-woocommerce-rest-api.php
+++ b/mailchimp-for-woocommerce/includes/class-mailchimp-woocommerce-rest-api.php
@@ -1069,7 +1069,7 @@
// get the auth token from either a header, or the query string
$token = (string) $this->getAuthToken($request);
// get the token and pull out both the consumer key and consumer secret split by the :
- $parts = str_getcsv($token, ':');
+ $parts = str_getcsv($token, ':', '"', '');
// if we don't have 2 items, that's invalid
if (count($parts) !== 2) {
mailchimp_debug('authorize', "token invalid format", ['token_present' => !empty($token)]);
--- a/mailchimp-for-woocommerce/includes/class-mailchimp-woocommerce-service.php
+++ b/mailchimp-for-woocommerce/includes/class-mailchimp-woocommerce-service.php
@@ -16,6 +16,7 @@
protected $cart_subscribe = null;
protected $force_cart_post = false;
protected $cart_was_submitted = false;
+ protected $cart_was_deleted = false;
protected $cart = array();
protected $validated_cart_db = false;
// this is used during rest api requests to force the user update through the is_admin function
@@ -193,10 +194,113 @@
{
if ($user_email = $this->getCurrentUserEmail()) {
$this->deleteCart(mailchimp_hash_trim_lower($user_email));
+ $this->cart_was_deleted = true;
}
}
/**
+ * Fired on woocommerce_cart_item_removed.
+ *
+ * WooCommerce fires this before WC_Cart::calculate_totals() (priority 20) has run, and the
+ * session copy of the cart is only rewritten by WC_Cart_Session::set_session() on
+ * woocommerce_after_calculate_totals. Reading the session here would hand us the pre-removal
+ * contents, so removing the last item looked like a normal update and the cart got deleted
+ * and immediately re-added in Mailchimp. Read the live cart object instead.
+ *
+ * @param null $cart_item_key
+ *
+ * @return bool|mixed|null
+ * @throws MailChimp_WooCommerce_Error
+ * @throws MailChimp_WooCommerce_RateLimitError
+ * @throws MailChimp_WooCommerce_ServerError
+ */
+ public function handleCartItemRemoved($cart_item_key = null)
+ {
+ $cart = $this->getCartItems(true);
+
+ // if we couldn't read the live cart don't guess, and definitely don't delete anything.
+ if (!is_array($cart)) {
+ return false;
+ }
+
+ $this->cart = $cart;
+
+ if (empty($cart)) {
+ return $this->handleCartEmptied();
+ }
+
+ return $this->handleCartUpdated();
+ }
+
+ /**
+ * Fired on woocommerce_cart_emptied, and from handleCartItemRemoved() when the last item goes.
+ *
+ * Nothing else was listening for this, so an "empty cart" action left the abandoned cart sitting
+ * in Mailchimp and the shopper kept getting "you left something behind" emails.
+ *
+ * Careful with $clear_persistent_cart: WooCommerce also empties the cart on logout and when it
+ * throws away an invalid session cookie (WC_Session_Handler::forget_session), and those go
+ * through wc_empty_cart() which passes false so the persistent cart in usermeta survives. That
+ * shopper has not abandoned anything, so we leave their Mailchimp cart alone. A real empty -
+ * the Store API "remove all items" route, a post-payment clear, or an empty-cart plugin calling
+ * WC()->cart->empty_cart() - passes true.
+ *
+ * @param bool $clear_persistent_cart
+ *
+ * @return bool
+ * @throws MailChimp_WooCommerce_Error
+ * @throws MailChimp_WooCommerce_RateLimitError
+ * @throws MailChimp_WooCommerce_ServerError
+ */
+ public function handleCartEmptied($clear_persistent_cart = true)
+ {
+ if (mailchimp_carts_disabled() || $this->is_admin || !mailchimp_is_configured()) {
+ return false;
+ }
+
+ // logout / session teardown - not an abandoned cart.
+ if (!$clear_persistent_cart || doing_action('wp_logout')) {
+ return false;
+ }
+
+ // the order flow already tears the cart down (onNewOrder + the Single_Order job), and
+ // WC_Checkout empties the cart right after, so don't pay for a second delete at checkout.
+ if ($this->cart_was_deleted) {
+ return false;
+ }
+
+ if (!($user_email = $this->getCurrentUserEmail())) {
+ return false;
+ }
+
+ if (mailchimp_email_is_privacy_protected($user_email)) {
+ return false;
+ }
+
+ $uid = mailchimp_hash_trim_lower($user_email);
+
+ // trackCart() writes the local row every time we post a cart, so no row means there is
+ // nothing in Mailchimp to delete. wc_clear_cart_after_payment() empties the cart on every
+ // single order-received page load, so without this we would fire a DELETE on each refresh.
+ if ($this->validated_cart_db && !$this->getCart($uid)) {
+ return false;
+ }
+
+ $this->cart = array();
+ $this->cart_was_deleted = true;
+
+ // drop the local row too, otherwise a later ?mc_cart_id= click re-hydrates the emptied
+ // cart into the woo session and pushes it straight back up to Mailchimp.
+ $this->deleteCart($uid);
+
+ if ($this->api()->deleteCartByID($this->getUniqueStoreID(), $uid)) {
+ mailchimp_log('ac.cart_emptied', "Deleted cart [$user_email] :: ID [$uid]");
+ }
+
+ return true;
+ }
+
+ /**
* @param null $updated
*
* @return bool|mixed|null
@@ -284,6 +388,11 @@
$handler->setStatus($this->cart_subscribe);
$handler->prepend_to_queue = true;
mailchimp_handle_or_queue($handler);
+ } else {
+ // the cart is empty - the remote delete above already ran, but the local row has to
+ // go as well or a ?mc_cart_id= click will re-hydrate the emptied cart and re-post it.
+ $this->deleteCart($uid);
+ $this->cart_was_deleted = true;
}
return !is_null($updated) ? $updated : true;
@@ -736,7 +845,18 @@
{
if (!mailchimp_is_configured()) return;
- $subscribed = (bool) isset($_POST['mailchimp_woocommerce_newsletter']) && $_POST['mailchimp_woocommerce_newsletter'];
+
+ if (isset($_POST['mailchimp_woocommerce_newsletter'])) {
+ $subscribed = (bool)isset($_POST['mailchimp_woocommerce_newsletter']) && $_POST['mailchimp_woocommerce_newsletter'];
+ } else {
+ // if no status posted, get the status from existing MC users
+ $user = new WP_User($user_id);
+ $email = $user->user_email;
+
+ $status = mailchimp_get_subscriber_status($email);
+
+ $subscribed = $status === 'subscribed';
+ }
if (isset($_POST['mailchimp_woocommerce_newsletter']) && $_POST['mailchimp_woocommerce_newsletter']) {
$gdpr_fields = isset($_POST['mailchimp_woocommerce_gdpr']) ?
@@ -833,10 +953,26 @@
}
/**
+ * @param bool $force_live read straight from the live WC_Cart object instead of the session copy.
+ * WooCommerce only writes the session copy on woocommerce_after_calculate_totals,
+ * so during woocommerce_cart_item_removed the session still holds the
+ * pre-removal contents and would make an emptied cart look populated.
* @return bool|array
*/
- public function getCartItems()
+ public function getCartItems($force_live = false)
{
+ if ($force_live) {
+ if (!function_exists('WC') || !($woo = WC()) || !$woo->cart) {
+ return $this->cart = false;
+ }
+ $cart_session = array();
+ foreach ($woo->cart->get_cart() as $key => $values) {
+ $cart_session[$key] = $values;
+ unset($cart_session[$key]['data']); // Unset product object
+ }
+ return $this->cart = $cart_session;
+ }
+
if (!($this->cart = $this->getWooSession('cart', false))) {
if (!function_exists('WC')) {
$this->cart = false;
--- a/mailchimp-for-woocommerce/includes/class-mailchimp-woocommerce-sms-consent.php
+++ b/mailchimp-for-woocommerce/includes/class-mailchimp-woocommerce-sms-consent.php
@@ -68,6 +68,15 @@
return static::$_instance;
}
+ public function applySmsFieldToRegisterForm($form)
+ {
+ $show_field = apply_filters('mailchimp_woocommerce_account_register_sms_field_field', true);
+
+ if ($show_field) {
+ $this->applyField($form);
+ }
+ }
+
/**
* @param $checkout
*/
@@ -79,9 +88,9 @@
}
// Check if merchant has approved SMS application
-// if (!$this->merchantHasSmsApproved()) {
-// return;
-// }
+ if (!$this->merchantHasSmsApproved()) {
+ return;
+ }
// Compliance: checkbox must always be unchecked by default, label and disclaimer are fixed
$sms_label = __('Text me with news and offers', 'mailchimp-for-woocommerce');
@@ -388,7 +397,7 @@
public static function isAllowedToUse()
{
- return mailchimp_sms_consent_enabled();
+ return mailchimp_sms_consent_active();
}
/**
@@ -400,6 +409,20 @@
}
/**
+ * @param $sanitized_user_login
+ * @param $user_email
+ * @param $reg_errors
+ */
+ public function processRegistrationForm($sanitized_user_login, $user_email, $reg_errors)
+ {
+ if (defined('WOOCOMMERCE_CHECKOUT')) {
+ return; // Ship checkout
+ }
+
+ $this->handleSmsStatus();
+ }
+
+ /**
* Handle SMS subscription status from classic checkout
*
* @param null $order_id
--- a/mailchimp-for-woocommerce/includes/class-mailchimp-woocommerce.php
+++ b/mailchimp-for-woocommerce/includes/class-mailchimp-woocommerce.php
@@ -92,7 +92,7 @@
$username = $is_options && array_key_exists('mailchimp_account_info_username', $plugin_options) ?
$plugin_options['mailchimp_account_info_username'] : false;
- $api_key_parts = str_getcsv($api_key, '-');
+ $api_key_parts = str_getcsv($api_key, '-', '"', '');
$data_center = isset($api_key_parts[1]) ? $api_key_parts[1] : 'us1';
return static::$logging_config = (object)array(
@@ -359,11 +359,12 @@
$this->loader->add_action($render_on, $service, 'applyNewsletterField');
$this->loader->add_action('woocommerce_ppe_checkout_order_review', $service, 'applyNewsletterField');
- $this->loader->add_action('woocommerce_register_form', $service, 'applyNewsletterFieldToRegisterForm');
$this->loader->add_action('woocommerce_checkout_order_processed', $service, 'processNewsletterField', 10, 2);
$this->loader->add_action('woocommerce_ppe_do_payaction', $service, 'processPayPalNewsletterField');
- $this->loader->add_action('woocommerce_register_post', $service, 'processRegistrationForm', 10, 3);
+
+ $this->loader->add_action('woocommerce_register_form', $service, 'applyNewsletterFieldToRegisterForm');
+ $this->loader->add_action('woocommerce_register_post', $service, 'processRegistrationForm', 10, 3);
}
}
@@ -376,13 +377,17 @@
$sms_consent->setVersion($this->version);
$render_on = $sms_consent->getOption('mailchimp_sms_consent_checkbox_action', 'woocommerce_after_checkout_billing_form');
- $sms_consent_allowed = MailChimp_Sms_Consent::isAllowedToUse();
+ $sms_consent_allowed = mailchimp_sms_consent_enabled();
if ($sms_consent_allowed) {
$this->loader->add_action($render_on, $sms_consent, 'applyField');
$this->loader->add_action('woocommerce_checkout_order_processed', $sms_consent, 'processSmsConsentField', 10, 2);
$this->loader->add_action('woocommerce_ppe_do_payaction', $sms_consent, 'processPayPalSmsConsentField');
+ $this->loader->add_action('woocommerce_register_post', $sms_consent, 'processRegistrationForm', 10, 3);
+
+ $this->loader->add_action('woocommerce_register_form', $sms_consent, 'applySmsFieldToRegisterForm');
+ $this->loader->add_action('woocommerce_register_post', $sms_consent, 'processRegistrationForm', 10, 3);
}
}
}
@@ -424,7 +429,8 @@
$this->loader->add_filter('woocommerce_update_cart_action_cart_updated', $service, 'handleCartUpdated');
$this->loader->add_action('woocommerce_cart_item_set_quantity', $service, 'handleCartUpdated');
$this->loader->add_action('woocommerce_add_to_cart', $service, 'handleCartUpdated');
- $this->loader->add_action('woocommerce_cart_item_removed', $service, 'handleCartUpdated');
+ $this->loader->add_action('woocommerce_cart_item_removed', $service, 'handleCartItemRemoved');
+ $this->loader->add_action('woocommerce_cart_emptied', $service, 'handleCartEmptied');
// save post hooks
$this->loader->add_action('woocommerce_new_order', $service, 'handleOrderCreate', 200, 2);
--- a/mailchimp-for-woocommerce/includes/processes/class-mailchimp-woocommerce-abstract-sync.php
+++ b/mailchimp-for-woocommerce/includes/processes/class-mailchimp-woocommerce-abstract-sync.php
@@ -209,7 +209,9 @@
foreach ($page->items as $resource) {
switch ($this->getResourceType()) {
case 'customers':
- mailchimp_handle_or_queue(new MailChimp_Woocommerce_Single_Customer($resource));
+ $customer = new MailChimp_Woocommerce_Single_Customer($resource);
+ $customer->set_full_sync(true);
+ mailchimp_handle_or_queue($customer);
break;
case 'coupons':
mailchimp_handle_or_queue(new MailChimp_WooCommerce_SingleCoupon($resource));
--- a/mailchimp-for-woocommerce/includes/processes/class-mailchimp-woocommerce-full-sync-manager.php
+++ b/mailchimp-for-woocommerce/includes/processes/class-mailchimp-woocommerce-full-sync-manager.php
@@ -100,6 +100,9 @@
$sync_started_at = Mailchimp_Woocommerce_DB_Helpers::get_option('mailchimp-woocommerce-sync.started_at');
$sync_completed_at = Mailchimp_Woocommerce_DB_Helpers::get_option('mailchimp-woocommerce-sync.completed_at');
+ // delete the initial sync flag
+ Mailchimp_Woocommerce_DB_Helpers::delete_option("mailchimp-woocommerce-sync.initial_sync");
+
$sync_total_time = $sync_completed_at - $sync_started_at;
$time = gmdate("H:i:s",$sync_total_time);
--- a/mailchimp-for-woocommerce/includes/processes/class-mailchimp-woocommerce-single-customer.php
+++ b/mailchimp-for-woocommerce/includes/processes/class-mailchimp-woocommerce-single-customer.php
@@ -4,6 +4,7 @@
{
public $customer_data;
public $id;
+ public $is_full_sync = false;
public function __construct($customer_lookup)
{
@@ -11,6 +12,18 @@
$this->id = $this->customer_data->customer_id;
}
+ /**
+ * @param $is_full_sync
+ *
+ * @return $this
+ */
+ public function set_full_sync($is_full_sync)
+ {
+ $this->is_full_sync = $is_full_sync;
+
+ return $this;
+ }
+
public function handle()
{
$this->process();
@@ -29,6 +42,9 @@
return false;
}
+ // make sure we tell the system this is a sync job and not a live job
+ $api->setIsSyncing($this->is_full_sync);
+
$email = $this->customer_data->email;
// make sure we don't need to skip this email
--- a/mailchimp-for-woocommerce/includes/processes/class-mailchimp-woocommerce-single-order.php
+++ b/mailchimp-for-woocommerce/includes/processes/class-mailchimp-woocommerce-single-order.php
@@ -99,6 +99,9 @@
return false;
}
+ // make sure we tell the system this is a sync job and not a live job
+ $api->setIsSyncing($this->is_full_sync);
+
$store_id = mailchimp_get_store_id();
// this will set the woo_order variable or return false.
--- a/mailchimp-for-woocommerce/mailchimp-woocommerce.php
+++ b/mailchimp-for-woocommerce/mailchimp-woocommerce.php
@@ -16,7 +16,7 @@
* Plugin Name: Mailchimp for WooCommerce
* Plugin URI: https://mailchimp.com/connect-your-store/
* Description: Connects WooCommerce to Mailchimp to sync your store data, send targeted campaigns to your customers, and sell more stuff.
- * Version: 6.1.1
+ * Version: 6.2
* Author: Mailchimp
* Author URI: https://mailchimp.com
* License: GPL-2.0+
@@ -27,7 +27,7 @@
* Requires at least: 6.2
* Tested up to: 7.0
* WC requires at least: 8.2
- * WC tested up to: 10.8
+ * WC tested up to: 11.0
*/
// If this file is called directly, abort.