Commit 62c950c9f58 for woocommerce
commit 62c950c9f585319d75d2f3e40c38fb6f0ecdd6d1
Author: Vlad Olaru <vlad.olaru@automattic.com>
Date: Thu Jul 23 09:54:52 2026 +0300
Fix malformed gateway settings transition events (#66912)
* fix: preserve gateway state for malformed settings
Gateway transition tracking must infer state when stored settings are malformed without changing the persisted option.
The previous fallback always treated a malformed old value as disabled and skipped malformed new values. This could emit a spurious enable notification or Tracks event, or miss a disable transition while corrupted settings were repaired.
Normalize only transition-comparison inputs to the gateway enabled-field default, falling back to disabled when the field is absent. Keep the add-option sentinel and raw stored values unchanged, with regression coverage for symmetric malformed transitions.
Refs #66861
* chore: add changelog for malformed gateway transitions
Document the patch-level behavior change that uses gateway defaults when evaluating transitions involving malformed settings.
Refs #66861
diff --git a/plugins/woocommerce/changelog/fix-66861-malformed-gateway-transition-defaults b/plugins/woocommerce/changelog/fix-66861-malformed-gateway-transition-defaults
new file mode 100644
index 00000000000..0dd9d3a83b9
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-66861-malformed-gateway-transition-defaults
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Use gateway defaults when detecting enabled-state transitions from malformed settings.
diff --git a/plugins/woocommerce/includes/class-wc-payment-gateways.php b/plugins/woocommerce/includes/class-wc-payment-gateways.php
index 3143e013092..0630bd1a1ab 100644
--- a/plugins/woocommerce/includes/class-wc-payment-gateways.php
+++ b/plugins/woocommerce/includes/class-wc-payment-gateways.php
@@ -195,17 +195,22 @@ class WC_Payment_Gateways {
if ( ! is_array( $value ) ) {
$logger->warning(
- sprintf( 'Payment gateway transition handling skipped because the new value for "%s" is not an array.', $option ),
+ sprintf( 'New payment gateway settings for "%s" were not an array; using gateway defaults for transition handling.', $option ),
array( 'source' => 'payment-gateways' )
);
- return;
}
- $logger->warning(
- sprintf( 'Previous payment gateway settings for "%s" were not an array; treating the gateway as disabled.', $option ),
- array( 'source' => 'payment-gateways' )
- );
- $old_value = array( 'enabled' => 'no' );
+ if ( null !== $old_value && ! is_array( $old_value ) ) {
+ $logger->warning(
+ sprintf( 'Previous payment gateway settings for "%s" were not an array; using gateway defaults for transition handling.', $option ),
+ array( 'source' => 'payment-gateways' )
+ );
+ }
+
+ $value = $this->normalize_gateway_settings_for_transition( $gateway, $value );
+ if ( null !== $old_value ) {
+ $old_value = $this->normalize_gateway_settings_for_transition( $gateway, $old_value );
+ }
}
if ( $this->was_gateway_enabled( $value, $old_value ) ) {
@@ -235,6 +240,25 @@ class WC_Payment_Gateways {
}
}
+ /**
+ * Normalize gateway settings for transition handling.
+ *
+ * @param WC_Payment_Gateway $gateway Payment gateway.
+ * @param mixed $settings Gateway settings.
+ * @return array Normalized gateway settings.
+ */
+ private function normalize_gateway_settings_for_transition( $gateway, $settings ) {
+ if ( is_array( $settings ) ) {
+ return $settings;
+ }
+
+ $form_fields = $gateway->get_form_fields();
+ $enabled_field = $form_fields['enabled'] ?? array();
+ $enabled_default = $gateway->get_field_default( $enabled_field );
+
+ return array( 'enabled' => 'yes' === $enabled_default ? 'yes' : 'no' );
+ }
+
/**
* Determines from changes in settings if a gateway was enabled.
*
diff --git a/plugins/woocommerce/tests/php/includes/class-wc-payment-gateways-test.php b/plugins/woocommerce/tests/php/includes/class-wc-payment-gateways-test.php
index f605b801509..7a43f657ebe 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-payment-gateways-test.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-payment-gateways-test.php
@@ -38,14 +38,38 @@ class WC_Payment_Gateways_Test extends WC_Unit_Test_Case {
}
/**
- * @testdox Gateway settings updates diagnose malformed values and track enabled repairs.
+ * @testdox Gateway settings option changes use gateway defaults for malformed transition values.
+ * @dataProvider provider_malformed_gateway_settings_transitions
+ *
+ * @param string|null $enabled_default The gateway's enabled form-field default, or null to remove the field.
+ * @param mixed $old_value The previously stored option value, or null for an added option.
+ * @param mixed $new_value The option value to add or update.
+ * @param string[] $warning_sides The malformed transition sides expected to produce warnings.
+ * @param int $info_count The expected number of informational log entries.
+ * @param int $action_count The expected number of gateway-enabled actions.
+ * @param int $enable_tracks_count The expected number of gateway-enable Tracks events.
+ * @param int $disable_tracks_count The expected number of gateway-disable Tracks events.
*/
- public function test_gateway_settings_updates_handle_non_array_values(): void {
- $option_name = 'woocommerce_cod_settings';
- $disabled_settings = array( 'enabled' => 'no' );
- $enabled_settings = array( 'enabled' => 'yes' );
- $tracking_option = get_option( 'woocommerce_allow_tracking', null );
- $current_user_id = get_current_user_id();
+ public function test_gateway_settings_option_changes_handle_non_array_values(
+ ?string $enabled_default,
+ $old_value,
+ $new_value,
+ array $warning_sides,
+ int $info_count,
+ int $action_count,
+ int $enable_tracks_count,
+ int $disable_tracks_count
+ ): void {
+ $option_name = 'woocommerce_cod_settings';
+ $gateway = $this->sut->payment_gateways()['cod'];
+ $tracking_option = get_option( 'woocommerce_allow_tracking', null );
+ $current_user_id = get_current_user_id();
+
+ if ( null === $enabled_default ) {
+ unset( $gateway->form_fields['enabled'] );
+ } else {
+ $gateway->form_fields['enabled']['default'] = $enabled_default;
+ }
// phpcs:disable Squiz.Commenting
$fake_logger = new class() {
@@ -88,44 +112,50 @@ class WC_Payment_Gateways_Test extends WC_Unit_Test_Case {
try {
wp_set_current_user( 0 );
update_option( 'woocommerce_allow_tracking', 'yes' );
- $this->clear_tracks_events();
delete_option( $option_name );
- add_option( $option_name, $disabled_settings );
-
- update_option( $option_name, '{"enabled":"yes"}' );
- $malformed_value = get_option( $option_name );
- $malformed_info_count = count( $fake_logger->infos );
- $malformed_action_count = count( $enabled_gateways );
- $malformed_tracks_count = count( $this->get_tracks_events( 'wcadmin_settings_payments_provider_enable' ) );
-
- update_option( $option_name, $enabled_settings );
-
- $this->assertSame( '{"enabled":"yes"}', $malformed_value, 'The malformed gateway settings should remain a string.' );
- $this->assertSame( 0, $malformed_info_count, 'A malformed new value should not log an enable transition.' );
- $this->assertSame( 0, $malformed_action_count, 'A malformed new value should not fire the gateway-enabled action.' );
- $this->assertSame( 0, $malformed_tracks_count, 'A malformed new value should not record an enable event.' );
-
- $this->assertSame( $enabled_settings, get_option( $option_name ), 'Valid gateway settings should be stored after a malformed value.' );
- $this->assertCount( 2, $fake_logger->warnings, 'Both malformed transition values should produce diagnostic warnings.' );
- $this->assertSame(
- 'Payment gateway transition handling skipped because the new value for "woocommerce_cod_settings" is not an array.',
- $fake_logger->warnings[0]['message'],
- 'The malformed new value warning should identify the affected option.'
- );
- $this->assertSame(
- 'Previous payment gateway settings for "woocommerce_cod_settings" were not an array; treating the gateway as disabled.',
- $fake_logger->warnings[1]['message'],
- 'The malformed old value warning should describe the repair behavior.'
- );
- $this->assertCount( 1, $fake_logger->infos, 'Repairing to enabled settings should log one enable transition.' );
- $this->assertCount( 1, $enabled_gateways, 'Repairing to enabled settings should fire the gateway-enabled action once.' );
- $this->assertSame( 'cod', $enabled_gateways[0]->id, 'The gateway-enabled action should receive the repaired gateway.' );
+ if ( null === $old_value ) {
+ $this->clear_tracks_events();
+ add_option( $option_name, $new_value );
+ } else {
+ add_option( $option_name, $old_value );
+ $fake_logger->infos = array();
+ $fake_logger->warnings = array();
+ $enabled_gateways = array();
+ $this->clear_tracks_events();
+
+ update_option( $option_name, $new_value );
+ }
+
+ $this->assertSame( $new_value, get_option( $option_name ), 'The option hook should not rewrite or decode the stored settings value.' );
+ $this->assertCount( $info_count, $fake_logger->infos, 'The transition should produce the expected number of informational log entries.' );
+ $this->assertCount( $action_count, $enabled_gateways, 'The transition should fire the gateway-enabled action the expected number of times.' );
+ if ( 1 === $action_count ) {
+ $this->assertSame( $gateway, $enabled_gateways[0], 'The gateway-enabled action should receive the COD gateway instance.' );
+ }
$this->assertCount(
- 1,
+ $enable_tracks_count,
$this->get_tracks_events( 'wcadmin_settings_payments_provider_enable' ),
- 'Repairing to enabled settings should record one enable event.'
+ 'The transition should record the expected number of gateway-enable events.'
+ );
+ $this->assertCount(
+ $disable_tracks_count,
+ $this->get_tracks_events( 'wcadmin_settings_payments_provider_disable' ),
+ 'The transition should record the expected number of gateway-disable events.'
);
+ if ( 1 === $info_count ) {
+ $this->assertSame( 'Payment gateway enabled: "Cash on delivery"', $fake_logger->infos[0]['message'], 'The informational log should identify the enabled gateway.' );
+ }
+
+ $this->assertCount( count( $warning_sides ), $fake_logger->warnings, 'Each malformed transition side should produce one warning.' );
+ foreach ( $warning_sides as $warning_index => $warning_side ) {
+ $expected_warning = 'new' === $warning_side
+ ? sprintf( 'New payment gateway settings for "%s" were not an array; using gateway defaults for transition handling.', $option_name )
+ : sprintf( 'Previous payment gateway settings for "%s" were not an array; using gateway defaults for transition handling.', $option_name );
+
+ $this->assertSame( $expected_warning, $fake_logger->warnings[ $warning_index ]['message'], "The malformed {$warning_side} value warning should describe the fallback behavior." );
+ $this->assertSame( array( 'source' => 'payment-gateways' ), $fake_logger->warnings[ $warning_index ]['data'], "The malformed {$warning_side} value warning should use the payment-gateways source." );
+ }
} finally {
remove_action( 'woocommerce_payment_gateway_enabled', $action_watcher );
wc_get_container()->reset_replacement( \Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders::class );
@@ -140,6 +170,32 @@ class WC_Payment_Gateways_Test extends WC_Unit_Test_Case {
}
}
+ /**
+ * Provides malformed gateway settings transitions.
+ *
+ * @return array<string, array{0: string|null, 1: mixed, 2: mixed, 3: string[], 4: int, 5: int, 6: int, 7: int}>
+ */
+ public function provider_malformed_gateway_settings_transitions(): array {
+ $malformed_old = '{"enabled":"yes"}';
+ $malformed_new = '{"enabled":"no"}';
+ $disabled = array( 'enabled' => 'no' );
+ $enabled = array( 'enabled' => 'yes' );
+
+ return array(
+ 'default disabled: malformed to explicit disabled' => array( 'no', $malformed_old, $disabled, array( 'old' ), 0, 0, 0, 0 ),
+ 'default disabled: malformed to explicit enabled' => array( 'no', $malformed_old, $enabled, array( 'old' ), 1, 1, 1, 0 ),
+ 'default enabled: malformed to explicit enabled' => array( 'yes', $malformed_old, $enabled, array( 'old' ), 0, 0, 0, 0 ),
+ 'default enabled: malformed to explicit disabled' => array( 'yes', $malformed_old, $disabled, array( 'old' ), 0, 0, 0, 1 ),
+ 'default disabled: explicit disabled to malformed' => array( 'no', $disabled, $malformed_new, array( 'new' ), 0, 0, 0, 0 ),
+ 'default enabled: explicit disabled to malformed' => array( 'yes', $disabled, $malformed_new, array( 'new' ), 1, 1, 1, 0 ),
+ 'default disabled: explicit enabled to malformed' => array( 'no', $enabled, $malformed_new, array( 'new' ), 0, 0, 0, 1 ),
+ 'default enabled: explicit enabled to malformed' => array( 'yes', $enabled, $malformed_new, array( 'new' ), 0, 0, 0, 0 ),
+ 'default enabled: distinct malformed values' => array( 'yes', $malformed_old, $malformed_new, array( 'new', 'old' ), 0, 0, 0, 0 ),
+ 'missing enabled field: enabled to malformed' => array( null, $enabled, $malformed_new, array( 'new' ), 0, 0, 0, 1 ),
+ 'add malformed value with enabled default' => array( 'yes', null, $malformed_new, array( 'new' ), 0, 0, 0, 0 ),
+ );
+ }
+
/**
* @testdox Enabling a gateway fires the notification action and logs the event.
*/