Commit 76de3b614e8 for woocommerce

commit 76de3b614e891fd87eef92c016d6e517fbd2b004
Author: Lucio Giannotta <lucio.giannotta@a8c.com>
Date:   Mon May 18 23:24:50 2026 +0800

    Preserve ISBN-10 'X' check digit in GTIN product field (#64889)

    Preserve ISBN-10 'X' check digit in GTIN field

    The GTIN/UPC/EAN/ISBN field (`global_unique_id`) silently stripped
    the 'X' check digit from valid ISBN-10 identifiers (e.g. 157249042X),
    truncating them to invalid 9-digit values and breaking downstream
    sync to Google Merchant Center.

    Allow X/x — but only as the final character, per ISO 2108. X in any
    other position is rejected (it isn't valid for ISBN-10 and isn't
    used by any other format the field accepts).

    Fixes #64239

diff --git a/plugins/woocommerce/changelog/64889-fix-wooplug-6573-gtin-isbn-10 b/plugins/woocommerce/changelog/64889-fix-wooplug-6573-gtin-isbn-10
new file mode 100644
index 00000000000..53ea80ca6a1
--- /dev/null
+++ b/plugins/woocommerce/changelog/64889-fix-wooplug-6573-gtin-isbn-10
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Preserve the 'X' check digit in ISBN-10 identifiers entered in the GTIN/UPC/EAN/ISBN product field. The server-side sanitizer, classic editor validation, and block editor input pattern previously stripped or rejected 'X', silently truncating valid ISBN-10s such as 157249042X.
\ No newline at end of file
diff --git a/plugins/woocommerce/client/legacy/js/admin/woocommerce_admin.js b/plugins/woocommerce/client/legacy/js/admin/woocommerce_admin.js
index 22d12e14865..ce4154bed71 100644
--- a/plugins/woocommerce/client/legacy/js/admin/woocommerce_admin.js
+++ b/plugins/woocommerce/client/legacy/js/admin/woocommerce_admin.js
@@ -302,9 +302,8 @@
 				'keyup',
 				'input[type=text][name*=_global_unique_id]',
 				function () {
-					var global_unique_id = $( this ).val();
-
-					if ( /[^0-9\-]/.test( global_unique_id ) ) {
+					// X/x is only valid as the final ISBN-10 check digit (ISO 2108).
+					if ( ! /^[0-9\-]*[0-9Xx]?$/.test( $( this ).val() ) ) {
 						$( document.body ).triggerHandler( 'wc_add_error_tip', [
 							$( this ),
 							'i18n_global_unique_id_error',
@@ -322,17 +321,23 @@
 				'change',
 				'input[type=text][name*=_global_unique_id]',
 				function () {
-					var global_unique_id = $( this ).val();
-					$( this ).val(
-						global_unique_id
-							.replace( /[^0-9\-]/g, '' )
-							.replace( /^-+|-+$/g, '' )
-					);
+					var cleaned = $( this )
+						.val()
+						.replace( /[^0-9Xx\-]/g, '' )
+						.replace( /^-+|-+$/g, '' );
+					$( this ).val( cleaned );

-					$( document.body ).triggerHandler(
-						'wc_remove_error_tip',
-						[ $( this ), 'i18n_global_unique_id_error' ]
-					);
+					if ( ! /^[0-9\-]*[0-9Xx]?$/.test( cleaned ) ) {
+						$( document.body ).triggerHandler( 'wc_add_error_tip', [
+							$( this ),
+							'i18n_global_unique_id_error',
+						] );
+					} else {
+						$( document.body ).triggerHandler(
+							'wc_remove_error_tip',
+							[ $( this ), 'i18n_global_unique_id_error' ]
+						);
+					}
 				}
 			)

diff --git a/plugins/woocommerce/includes/abstracts/abstract-wc-product.php b/plugins/woocommerce/includes/abstracts/abstract-wc-product.php
index 7fb8497ce06..0c3cfc87859 100644
--- a/plugins/woocommerce/includes/abstracts/abstract-wc-product.php
+++ b/plugins/woocommerce/includes/abstracts/abstract-wc-product.php
@@ -893,7 +893,18 @@ class WC_Product extends WC_Abstract_Legacy_Product {
 	 * @return void
 	 */
 	public function set_global_unique_id( $global_unique_id ) {
-		$global_unique_id = preg_replace( '/[^0-9\-]/', '', (string) $global_unique_id );
+		// Strip characters that are never valid (digits, hyphens, and X/x for the ISBN-10 check digit).
+		$global_unique_id = preg_replace( '/[^0-9Xx\-]/', '', (string) $global_unique_id );
+
+		// X is only valid as the final ISBN-10 check digit character (ISO 2108) — reject anywhere else.
+		if ( $this->get_object_read() && ! empty( $global_unique_id ) && ! preg_match( '/^[0-9\-]*[0-9Xx]?$/', $global_unique_id ) ) {
+			$this->error(
+				'product_invalid_global_unique_id_format',
+				__( 'Invalid GTIN, UPC, EAN, or ISBN. The letter X is only valid as the final ISBN-10 check digit.', 'woocommerce' ),
+				400
+			);
+		}
+
 		if ( $this->get_object_read() && ! empty( $global_unique_id ) && ! wc_product_has_global_unique_id( $this->get_id(), $global_unique_id ) ) {
 			$global_unique_id_found = wc_get_product_id_by_global_unique_id( $global_unique_id );

diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-assets.php b/plugins/woocommerce/includes/admin/class-wc-admin-assets.php
index 61c43de3a3a..c029da935ef 100644
--- a/plugins/woocommerce/includes/admin/class-wc-admin-assets.php
+++ b/plugins/woocommerce/includes/admin/class-wc-admin-assets.php
@@ -491,7 +491,7 @@ if ( ! class_exists( 'WC_Admin_Assets', false ) ) :
 					'i18n_delete_product_notice'        => __( 'This product has produced sales and may be linked to existing orders. Are you sure you want to delete it?', 'woocommerce' ),
 					'i18n_remove_personal_data_notice'  => __( 'This action cannot be reversed. Are you sure you wish to erase personal data from the selected orders?', 'woocommerce' ),
 					'i18n_confirm_delete'               => __( 'Are you sure you wish to delete this item?', 'woocommerce' ),
-					'i18n_global_unique_id_error'       => __( 'Please enter only numbers and hyphens (-).', 'woocommerce' ),
+					'i18n_global_unique_id_error'       => __( 'Enter only numbers and hyphens (-). The letter X is allowed only as the final ISBN-10 check digit.', 'woocommerce' ),
 					'decimal_point'                     => $decimal,
 					'mon_decimal_point'                 => wc_get_price_decimal_separator(),
 					'ajax_url'                          => admin_url( 'admin-ajax.php' ),
diff --git a/plugins/woocommerce/src/Internal/Features/ProductBlockEditor/ProductTemplates/ProductVariationTemplate.php b/plugins/woocommerce/src/Internal/Features/ProductBlockEditor/ProductTemplates/ProductVariationTemplate.php
index 3ace4cb9f6e..53f785dca58 100644
--- a/plugins/woocommerce/src/Internal/Features/ProductBlockEditor/ProductTemplates/ProductVariationTemplate.php
+++ b/plugins/woocommerce/src/Internal/Features/ProductBlockEditor/ProductTemplates/ProductVariationTemplate.php
@@ -347,8 +347,8 @@ class ProductVariationTemplate extends AbstractProductFormTemplate implements Pr
 					'label'    => sprintf( __( '%1$s, %2$s, %3$s, or %4$s', 'woocommerce' ), '<abbr title="' . esc_attr__( 'Global Trade Item Number', 'woocommerce' ) . '">' . esc_html__( 'GTIN', 'woocommerce' ) . '</abbr>', '<abbr title="' . esc_attr__( 'Universal Product Code', 'woocommerce' ) . '">' . esc_html__( 'UPC', 'woocommerce' ) . '</abbr>', '<abbr title="' . esc_attr__( 'European Article Number', 'woocommerce' ) . '">' . esc_html__( 'EAN', 'woocommerce' ) . '</abbr>', '<abbr title="' . esc_attr__( 'International Standard Book Number', 'woocommerce' ) . '">' . esc_html__( 'ISBN', 'woocommerce' ) . '</abbr>' ),
 					'tooltip'  => __( 'Enter a barcode or any other identifier unique to this product. It can help you list this product on other channels or marketplaces.', 'woocommerce' ),
 					'pattern'  => array(
-						'value'   => '[0-9\-]*',
-						'message' => __( 'Please enter only numbers and hyphens (-).', 'woocommerce' ),
+						'value'   => '[0-9\-]*[0-9Xx]?',
+						'message' => __( 'Enter only numbers and hyphens (-). The letter X is allowed only as the final ISBN-10 check digit.', 'woocommerce' ),
 					),
 				),
 			)
diff --git a/plugins/woocommerce/src/Internal/Features/ProductBlockEditor/ProductTemplates/SimpleProductTemplate.php b/plugins/woocommerce/src/Internal/Features/ProductBlockEditor/ProductTemplates/SimpleProductTemplate.php
index f3d667568cd..220b0585279 100644
--- a/plugins/woocommerce/src/Internal/Features/ProductBlockEditor/ProductTemplates/SimpleProductTemplate.php
+++ b/plugins/woocommerce/src/Internal/Features/ProductBlockEditor/ProductTemplates/SimpleProductTemplate.php
@@ -784,8 +784,8 @@ class SimpleProductTemplate extends AbstractProductFormTemplate implements Produ
 					'label'    => sprintf( __( '%1$s, %2$s, %3$s, or %4$s', 'woocommerce' ), '<abbr title="' . esc_attr__( 'Global Trade Item Number', 'woocommerce' ) . '">' . esc_html__( 'GTIN', 'woocommerce' ) . '</abbr>', '<abbr title="' . esc_attr__( 'Universal Product Code', 'woocommerce' ) . '">' . esc_html__( 'UPC', 'woocommerce' ) . '</abbr>', '<abbr title="' . esc_attr__( 'European Article Number', 'woocommerce' ) . '">' . esc_html__( 'EAN', 'woocommerce' ) . '</abbr>', '<abbr title="' . esc_attr__( 'International Standard Book Number', 'woocommerce' ) . '">' . esc_html__( 'ISBN', 'woocommerce' ) . '</abbr>' ),
 					'tooltip'  => __( 'Enter a barcode or any other identifier unique to this product. It can help you list this product on other channels or marketplaces.', 'woocommerce' ),
 					'pattern'  => array(
-						'value'   => '[0-9\-]*',
-						'message' => __( 'Please enter only numbers and hyphens (-).', 'woocommerce' ),
+						'value'   => '[0-9\-]*[0-9Xx]?',
+						'message' => __( 'Enter only numbers and hyphens (-). The letter X is allowed only as the final ISBN-10 check digit.', 'woocommerce' ),
 					),
 				),
 				'disableConditions' => array(
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/product/product-simple.php b/plugins/woocommerce/tests/legacy/unit-tests/product/product-simple.php
index b94dec0eddf..ce80517278d 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/product/product-simple.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/product/product-simple.php
@@ -188,4 +188,33 @@ class WC_Tests_Product_Simple extends WC_Unit_Test_Case {
 		$this->product->set_manage_stock( 'no' );
 		$this->assertFalse( $this->product->backorders_require_notification() );
 	}
+
+	/**
+	 * Ensure set_global_unique_id() preserves the 'X' check digit in ISBN-10 identifiers.
+	 *
+	 * @testdox set_global_unique_id() preserves the X check digit in ISBN-10 values.
+	 */
+	public function test_set_global_unique_id_preserves_isbn10_x() {
+		$this->product->set_global_unique_id( '157249042X' );
+		$this->assertEquals( '157249042X', $this->product->get_global_unique_id() );
+
+		$this->product->set_global_unique_id( '125010856x' );
+		$this->assertEquals( '125010856x', $this->product->get_global_unique_id() );
+
+		// Characters other than digits, hyphens, and X/x are still stripped.
+		$this->product->set_global_unique_id( '978-3-16-148410-A' );
+		$this->assertEquals( '978-3-16-148410-', $this->product->get_global_unique_id() );
+	}
+
+	/**
+	 * Ensure set_global_unique_id() rejects values with X in non-final positions.
+	 *
+	 * Per ISO 2108, X is only valid as the ISBN-10 check digit (final character).
+	 *
+	 * @testdox set_global_unique_id() rejects X anywhere other than the final character.
+	 */
+	public function test_set_global_unique_id_rejects_x_mid_string() {
+		$this->expectException( WC_Data_Exception::class );
+		$this->product->set_global_unique_id( '15724X9042' );
+	}
 }