Commit 9c769eb92a0 for woocommerce

commit 9c769eb92a0c2bb7eba260769fdbaa7cc3ff080e
Author: Vlad Olaru <vlad.olaru@automattic.com>
Date:   Wed Jul 22 12:57:10 2026 +0300

    Fix fatal errors when updating malformed gateway settings (#66848)

    * fix: guard gateway option-change tracking

    Payment gateway option hooks can receive values of any type from WordPress. Passing a string to ArrayUtil caused a post-update fatal after the database write had already succeeded.

    Reject non-array transitions before notification and tracking logic, and cover malformed new and old values through the real option hook boundary.

    Refs #66828

    * chore: add gateway tracking fix changelog

    The gateway settings hardening ships in WooCommerce Core and needs release metadata. Without an entry, the fix would be omitted from the generated changelog.

    Add a patch-level fix entry describing the fatal-error prevention.

    Refs #66828

    * fix: track gateway enablement after settings repairs

    Payment gateway settings can be repaired from malformed option values after external writers persist a non-array value.

    The previous guard skipped transition handling whenever the old value was malformed. This prevented enable logging, notifications, and analytics even when a repair wrote valid enabled settings, while invalid new values failed silently.

    Treat malformed old settings as the effective disabled state so valid repairs follow normal transitions. Diagnose both rejected new values and recovered old values without logging their contents, and pin the side effects in the existing regression test.

    Refs #66828

diff --git a/plugins/woocommerce/changelog/fix-66828-non-array-gateway-settings b/plugins/woocommerce/changelog/fix-66828-non-array-gateway-settings
new file mode 100644
index 00000000000..9cf83e62b86
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-66828-non-array-gateway-settings
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Prevent fatal errors when updating malformed payment gateway settings.
diff --git a/plugins/woocommerce/includes/class-wc-payment-gateways.php b/plugins/woocommerce/includes/class-wc-payment-gateways.php
index baac33a4d4f..3143e013092 100644
--- a/plugins/woocommerce/includes/class-wc-payment-gateways.php
+++ b/plugins/woocommerce/includes/class-wc-payment-gateways.php
@@ -190,6 +190,24 @@ class WC_Payment_Gateways {
 	 * @since 8.5.0
 	 */
 	private function payment_gateway_settings_option_changed( $gateway, $value, $option, $old_value = null ) {
+		if ( ! is_array( $value ) || ( null !== $old_value && ! is_array( $old_value ) ) ) {
+			$logger = wc_get_container()->get( LegacyProxy::class )->call_function( 'wc_get_logger' );
+
+			if ( ! is_array( $value ) ) {
+				$logger->warning(
+					sprintf( 'Payment gateway transition handling skipped because the new value for "%s" is not an array.', $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 ( $this->was_gateway_enabled( $value, $old_value ) ) {
 			$logger = wc_get_container()->get( LegacyProxy::class )->call_function( 'wc_get_logger' );
 			$logger->info( sprintf( 'Payment gateway enabled: "%s"', $gateway->get_method_title() ) );
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 fe87fb9f3d9..f605b801509 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
@@ -37,6 +37,109 @@ class WC_Payment_Gateways_Test extends WC_Unit_Test_Case {
 		delete_option( 'jetpack_activation_source' );
 	}

+	/**
+	 * @testdox Gateway settings updates diagnose malformed values and track enabled repairs.
+	 */
+	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();
+
+		// phpcs:disable Squiz.Commenting
+		$fake_logger = new class() {
+			public $infos    = array();
+			public $warnings = array();
+
+			public function info( $message, $data = array() ) {
+				$this->infos[] = array(
+					'message' => $message,
+					'data'    => $data,
+				);
+			}
+
+			public function warning( $message, $data = array() ) {
+				$this->warnings[] = array(
+					'message' => $message,
+					'data'    => $data,
+				);
+			}
+		};
+		// phpcs:enable Squiz.Commenting
+		$this->register_legacy_proxy_function_mocks(
+			array(
+				'wc_get_logger' => function () use ( $fake_logger ) {
+					return $fake_logger;
+				},
+			)
+		);
+
+		$providers_service = $this->createStub( \Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders::class );
+		$providers_service->method( 'get_payment_gateway_details' )->willReturn( array() );
+		wc_get_container()->replace( \Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders::class, $providers_service );
+
+		$enabled_gateways = array();
+		$action_watcher   = function ( $gateway ) use ( &$enabled_gateways ) {
+			$enabled_gateways[] = $gateway;
+		};
+		add_action( 'woocommerce_payment_gateway_enabled', $action_watcher );
+
+		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.' );
+			$this->assertCount(
+				1,
+				$this->get_tracks_events( 'wcadmin_settings_payments_provider_enable' ),
+				'Repairing to enabled settings should record one enable event.'
+			);
+		} finally {
+			remove_action( 'woocommerce_payment_gateway_enabled', $action_watcher );
+			wc_get_container()->reset_replacement( \Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders::class );
+			$this->clear_tracks_events();
+			wp_set_current_user( $current_user_id );
+
+			if ( null === $tracking_option ) {
+				delete_option( 'woocommerce_allow_tracking' );
+			} else {
+				update_option( 'woocommerce_allow_tracking', $tracking_option );
+			}
+		}
+	}
+
 	/**
 	 * @testdox Enabling a gateway fires the notification action and logs the event.
 	 */