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' );
+ }
}