Commit 6c00db89bd9 for woocommerce

commit 6c00db89bd94d8bc8bc44e96167da27aa0de2952
Author: woocommercebot <30233865+woocommercebot@users.noreply.github.com>
Date:   Fri Jul 3 00:28:20 2026 +0530

    [Backport to trunk] Avoid update-time fatals in 10.9 settings paths (#66214)

    * Avoid update-time fatals in 10.9 settings paths (#66081)

    * Avoid update-time fatals: keep a Settings controller stub and guard new settings SDK classes

    * chore: add changelog for update fatal fix

    * fix: tolerate settings SDK autoload failures

    ---------

    Co-authored-by: Jorge Torres <jorge.torres@automattic.com>
    Co-authored-by: Cvetan Cvetanov <cvetan.cvetanov@automattic.com>

    * Remove changelog file for the backported 10.9.2 fix

    ---------

    Co-authored-by: Daniel Mallory <daniel.mallory@automattic.com>
    Co-authored-by: Jorge Torres <jorge.torres@automattic.com>
    Co-authored-by: Cvetan Cvetanov <cvetan.cvetanov@automattic.com>

diff --git a/plugins/woocommerce/includes/admin/settings/class-wc-settings-page.php b/plugins/woocommerce/includes/admin/settings/class-wc-settings-page.php
index aeb457063aa..d89f14908b4 100644
--- a/plugins/woocommerce/includes/admin/settings/class-wc-settings-page.php
+++ b/plugins/woocommerce/includes/admin/settings/class-wc-settings-page.php
@@ -139,9 +139,9 @@ if ( ! class_exists( 'WC_Settings_Page', false ) ) :
 			}

 			$section = is_string( $current_section ) ? $current_section : '';
-			$context = SettingsUIRequestContext::for_settings_page( $this, $section );
+			$context = $this->get_settings_ui_request_context( $section );

-			if ( ! $context->is_rendering_enabled() ) {
+			if ( ! $context || ! $context->is_rendering_enabled() ) {
 				return $classes;
 			}

@@ -253,6 +253,44 @@ if ( ! class_exists( 'WC_Settings_Page', false ) ) :
 			return apply_filters( 'woocommerce_get_settings_' . $this->id, $settings, $section_id );
 		}

+		/**
+		 * Settings section registry instance, or null when the modern settings SDK is unavailable.
+		 *
+		 * The class can be missing mid-update: a 10.9 copy of this file may load before the autoloader
+		 * class map can safely resolve the new registry, so a direct call would fatal.
+		 *
+		 * @return SettingsSectionRegistry|null
+		 */
+		protected function get_settings_section_registry() {
+			try {
+				if ( ! class_exists( SettingsSectionRegistry::class ) ) {
+					return null;
+				}
+
+				return SettingsSectionRegistry::get_instance();
+			} catch ( \Throwable $e ) {
+				return null;
+			}
+		}
+
+		/**
+		 * Settings UI request context, or null when the modern settings SDK is unavailable.
+		 *
+		 * @param string $section Section id.
+		 * @return SettingsUIRequestContext|null
+		 */
+		protected function get_settings_ui_request_context( $section ) {
+			try {
+				if ( ! class_exists( SettingsUIRequestContext::class ) ) {
+					return null;
+				}
+
+				return SettingsUIRequestContext::for_settings_page( $this, $section );
+			} catch ( \Throwable $e ) {
+				return null;
+			}
+		}
+
 		/**
 		 * Get the settings for a given section.
 		 * This method is invoked from 'get_settings_for_section' when no 'get_settings_for_{current_section}_section'
@@ -266,7 +304,8 @@ if ( ! class_exists( 'WC_Settings_Page', false ) ) :
 		 * @return array Settings array, each item being an associative array representing a setting.
 		 */
 		protected function get_settings_for_section_core( $section_id ) {
-			$registered_section = SettingsSectionRegistry::get_instance()->get_registered( $this->id, (string) $section_id );
+			$registry           = $this->get_settings_section_registry();
+			$registered_section = $registry ? $registry->get_registered( $this->id, (string) $section_id ) : null;

 			return $registered_section ? $registered_section->get_settings( $this ) : array();
 		}
@@ -278,7 +317,8 @@ if ( ! class_exists( 'WC_Settings_Page', false ) ) :
 		 */
 		public function get_sections() {
 			$sections            = $this->get_own_sections();
-			$registered_sections = SettingsSectionRegistry::get_instance()->get_sections_for_page( $this->id );
+			$registry            = $this->get_settings_section_registry();
+			$registered_sections = $registry ? $registry->get_sections_for_page( $this->id ) : array();

 			foreach ( $registered_sections as $section_id => $section_label ) {
 				// Preserve sections declared by the settings page when a registered section uses the same id.
@@ -349,9 +389,9 @@ if ( ! class_exists( 'WC_Settings_Page', false ) ) :
 			global $current_section;

 			$section = is_string( $current_section ) ? $current_section : '';
-			$context = SettingsUIRequestContext::for_settings_page( $this, $section );
+			$context = $this->get_settings_ui_request_context( $section );

-			if ( $context->is_rendering_enabled() ) {
+			if ( $context && $context->is_rendering_enabled() ) {
 				$settings_ui_page = $context->get_settings_ui_page();
 				assert( $settings_ui_page instanceof SettingsUIPageInterface );

diff --git a/plugins/woocommerce/includes/admin/settings/class-wc-settings-payment-gateways.php b/plugins/woocommerce/includes/admin/settings/class-wc-settings-payment-gateways.php
index dc6df1ae1bf..1dd6937dffe 100644
--- a/plugins/woocommerce/includes/admin/settings/class-wc-settings-payment-gateways.php
+++ b/plugins/woocommerce/includes/admin/settings/class-wc-settings-payment-gateways.php
@@ -240,7 +240,9 @@ class WC_Settings_Payment_Gateways extends WC_Settings_Page {
 			return false;
 		}

-		return null !== SettingsSectionRegistry::get_instance()->get_registered( self::TAB_NAME, (string) $section );
+		$registry = $this->get_settings_section_registry();
+
+		return null !== $registry && null !== $registry->get_registered( self::TAB_NAME, (string) $section );
 	}

 	/**
diff --git a/plugins/woocommerce/src/Admin/API/Settings.php b/plugins/woocommerce/src/Admin/API/Settings.php
new file mode 100644
index 00000000000..36e9fcea83f
--- /dev/null
+++ b/plugins/woocommerce/src/Admin/API/Settings.php
@@ -0,0 +1,30 @@
+<?php
+/**
+ * REST API Settings Controller (compatibility stub).
+ *
+ * @package WooCommerce\Admin\API
+ */
+
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Admin\API;
+
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Settings controller.
+ *
+ * The real controller was removed in 10.9 with the settings editor. This empty stub stays so an
+ * in-memory 10.8 controller list, still naming this class while the files are swapped to 10.9
+ * during an update, can instantiate it instead of fataling on the deleted file. It registers
+ * nothing.
+ *
+ * @deprecated 10.9.0
+ */
+class Settings {
+
+	/**
+	 * Register routes. Intentionally a no-op.
+	 */
+	public function register_routes(): void {}
+}
diff --git a/plugins/woocommerce/src/Internal/Admin/Settings.php b/plugins/woocommerce/src/Internal/Admin/Settings.php
index 0f19c7d1a59..5099942d857 100644
--- a/plugins/woocommerce/src/Internal/Admin/Settings.php
+++ b/plugins/woocommerce/src/Internal/Admin/Settings.php
@@ -416,7 +416,16 @@ class Settings {
 	 * @return array
 	 */
 	private function add_settings_ui_schema( array $settings ): array {
-		$context = SettingsUIRequestContext::get_current();
+		try {
+			if ( ! class_exists( SettingsUIRequestContext::class ) ) {
+				return $settings;
+			}
+
+			$context = SettingsUIRequestContext::get_current();
+		} catch ( \Throwable $e ) {
+			return $settings;
+		}
+
 		if ( ! $context ) {
 			return $settings;
 		}
diff --git a/plugins/woocommerce/src/Internal/Admin/Settings/SettingsUIRequestContext.php b/plugins/woocommerce/src/Internal/Admin/Settings/SettingsUIRequestContext.php
index dfb7c24dd6d..093fd925d8c 100644
--- a/plugins/woocommerce/src/Internal/Admin/Settings/SettingsUIRequestContext.php
+++ b/plugins/woocommerce/src/Internal/Admin/Settings/SettingsUIRequestContext.php
@@ -344,7 +344,11 @@ class SettingsUIRequestContext {
 	 * @return SettingsUIPageInterface|null
 	 */
 	private static function resolve_settings_ui_page( \WC_Settings_Page $settings_page, string $section ): ?SettingsUIPageInterface {
-		$registered_section = SettingsSectionRegistry::get_instance()->get_registered( $settings_page->get_id(), $section );
+		try {
+			$registered_section = SettingsSectionRegistry::get_instance()->get_registered( $settings_page->get_id(), $section );
+		} catch ( \Throwable $e ) {
+			$registered_section = null;
+		}

 		if ( $registered_section ) {
 			return new RegisteredSettingsSectionAdapter( $settings_page, $registered_section );
diff --git a/plugins/woocommerce/src/Internal/Admin/WCAdminAssets.php b/plugins/woocommerce/src/Internal/Admin/WCAdminAssets.php
index 65aa7ea2e31..3ce46b2a440 100644
--- a/plugins/woocommerce/src/Internal/Admin/WCAdminAssets.php
+++ b/plugins/woocommerce/src/Internal/Admin/WCAdminAssets.php
@@ -433,7 +433,16 @@ class WCAdminAssets {
 	 * @return array
 	 */
 	private function get_settings_ui_script_dependencies(): array {
-		$context = SettingsUIRequestContext::get_current();
+		try {
+			if ( ! class_exists( SettingsUIRequestContext::class ) ) {
+				return array();
+			}
+
+			$context = SettingsUIRequestContext::get_current();
+		} catch ( \Throwable $e ) {
+			return array();
+		}
+
 		if ( ! $context ) {
 			return array();
 		}