Commit 56465145b19 for woocommerce
commit 56465145b19f73af3fc6f00630e27bfab501cd55
Author: Tung Du <dinhtungdu@gmail.com>
Date: Mon Aug 3 16:25:24 2026 +0700
Fix: prevent invalid date in Quick Edit affect sale schedule (#67052)
* fix: preserve Quick Edit sale schedules
* chore: add Quick Edit sale schedule changelog
* fix: strictly validate Quick Edit sale dates
diff --git a/plugins/woocommerce/changelog/fix-67048-quick-edit-sale-date-validation b/plugins/woocommerce/changelog/fix-67048-quick-edit-sale-date-validation
new file mode 100644
index 00000000000..cdce3480a72
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-67048-quick-edit-sale-date-validation
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Prevent invalid Quick Edit sale dates and unavailable datepicker scripts from corrupting product pricing schedules.
diff --git a/plugins/woocommerce/client/legacy/js/admin/quick-edit.js b/plugins/woocommerce/client/legacy/js/admin/quick-edit.js
index eee4e8880e2..86d94b95722 100644
--- a/plugins/woocommerce/client/legacy/js/admin/quick-edit.js
+++ b/plugins/woocommerce/client/legacy/js/admin/quick-edit.js
@@ -2,6 +2,10 @@
jQuery(
function( $ ) {
function init_sale_datepickers( $row ) {
+ if ( ! $.fn.datepicker ) {
+ return;
+ }
+
var $date_from = $row.find( 'input[name="_sale_price_dates_from"]' ),
$date_to = $row.find( 'input[name="_sale_price_dates_to"]' );
diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-assets.php b/plugins/woocommerce/includes/admin/class-wc-admin-assets.php
index 54367f172b0..b9830c3bcbd 100644
--- a/plugins/woocommerce/includes/admin/class-wc-admin-assets.php
+++ b/plugins/woocommerce/includes/admin/class-wc-admin-assets.php
@@ -519,7 +519,8 @@ if ( ! class_exists( 'WC_Admin_Assets', false ) ) :
// Products.
if ( in_array( $screen_id, array( 'edit-product' ) ) ) {
- wp_enqueue_script( 'woocommerce_quick-edit', WC()->plugin_url() . '/assets/js/admin/quick-edit' . $suffix . '.js', array( 'jquery', 'jquery-ui-datepicker', 'woocommerce_admin' ), $version, false );
+ wp_enqueue_script( 'jquery-ui-datepicker' );
+ wp_enqueue_script( 'woocommerce_quick-edit', WC()->plugin_url() . '/assets/js/admin/quick-edit' . $suffix . '.js', array( 'jquery', 'woocommerce_admin' ), $version, false );
$params = array(
'strings' => array(
diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-post-types.php b/plugins/woocommerce/includes/admin/class-wc-admin-post-types.php
index adcd8af7472..21fde9f9e92 100644
--- a/plugins/woocommerce/includes/admin/class-wc-admin-post-types.php
+++ b/plugins/woocommerce/includes/admin/class-wc-admin-post-types.php
@@ -11,6 +11,7 @@ use Automattic\WooCommerce\Enums\ProductStockStatus;
use Automattic\WooCommerce\Enums\ProductType;
use Automattic\WooCommerce\Internal\CostOfGoodsSold\CostOfGoodsSoldController;
use Automattic\WooCommerce\Utilities\NumberUtil;
+use Automattic\WooCommerce\Utilities\TimeUtil;
if ( ! defined( 'ABSPATH' ) ) {
exit;
@@ -463,41 +464,43 @@ class WC_Admin_Post_Types {
$product->set_sale_price( $sale_price );
}
- // Match the full product editor's date parsing and site-timezone behavior.
- if ( isset( $request_data['_sale_price_dates_from'] ) ) {
- $date_on_sale_from = '';
- if ( is_string( $request_data['_sale_price_dates_from'] ) ) {
- /**
- * Sanitized sale start date.
- *
- * @var string $date_on_sale_from
- */
- $date_on_sale_from = wc_clean( wp_unslash( $request_data['_sale_price_dates_from'] ) );
- }
-
- if ( ! empty( $date_on_sale_from ) ) {
- $date_on_sale_from = date( 'Y-m-d 00:00:00', (int) strtotime( $date_on_sale_from ) ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
+ // Parse valid dates using the full product editor's site-timezone behavior.
+ $submitted_sale_date_from = $request_data['_sale_price_dates_from'] ?? null;
+ if ( is_string( $submitted_sale_date_from ) ) {
+ /**
+ * Submitted sale start date.
+ *
+ * @var string $date_on_sale_from
+ */
+ $date_on_sale_from = wp_unslash( $submitted_sale_date_from );
+
+ if ( '' === $date_on_sale_from ) {
+ $product->set_date_on_sale_from( '' );
+ } elseif ( TimeUtil::is_valid_date( $date_on_sale_from, 'Y-m-d' ) ) {
+ $timestamp = strtotime( $date_on_sale_from );
+ if ( false !== $timestamp ) {
+ $product->set_date_on_sale_from( date( 'Y-m-d 00:00:00', $timestamp ) ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
+ }
}
-
- $product->set_date_on_sale_from( $date_on_sale_from );
}
- if ( isset( $request_data['_sale_price_dates_to'] ) ) {
- $date_on_sale_to = '';
- if ( is_string( $request_data['_sale_price_dates_to'] ) ) {
- /**
- * Sanitized sale end date.
- *
- * @var string $date_on_sale_to
- */
- $date_on_sale_to = wc_clean( wp_unslash( $request_data['_sale_price_dates_to'] ) );
- }
-
- if ( ! empty( $date_on_sale_to ) ) {
- $date_on_sale_to = date( 'Y-m-d 23:59:59', (int) strtotime( $date_on_sale_to ) ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
+ $submitted_sale_date_to = $request_data['_sale_price_dates_to'] ?? null;
+ if ( is_string( $submitted_sale_date_to ) ) {
+ /**
+ * Submitted sale end date.
+ *
+ * @var string $date_on_sale_to
+ */
+ $date_on_sale_to = wp_unslash( $submitted_sale_date_to );
+
+ if ( '' === $date_on_sale_to ) {
+ $product->set_date_on_sale_to( '' );
+ } elseif ( TimeUtil::is_valid_date( $date_on_sale_to, 'Y-m-d' ) ) {
+ $timestamp = strtotime( $date_on_sale_to );
+ if ( false !== $timestamp ) {
+ $product->set_date_on_sale_to( date( 'Y-m-d 23:59:59', $timestamp ) ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
+ }
}
-
- $product->set_date_on_sale_to( $date_on_sale_to );
}
}
diff --git a/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-assets-test.php b/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-assets-test.php
index 4fa1302a7bf..0a23cf9de64 100644
--- a/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-assets-test.php
+++ b/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-assets-test.php
@@ -30,10 +30,30 @@ class WC_Admin_Assets_Test extends WC_Unit_Test_Case {
public function tearDown(): void {
unset( $_GET['page'] );
wp_dequeue_script( 'woocommerce_admin' );
+ wp_dequeue_script( 'woocommerce_quick-edit' );
+ wp_dequeue_script( 'jquery-ui-datepicker' );
wp_dequeue_script( 'heartbeat' );
parent::tearDown();
}
+ /**
+ * @testdox Quick Edit remains loadable when the optional datepicker is unavailable.
+ */
+ public function test_quick_edit_does_not_depend_on_datepicker(): void {
+ set_current_screen();
+ $screen = get_current_screen();
+ $screen->id = 'edit-product';
+ $screen->base = 'edit';
+ $screen->post_type = 'product';
+
+ $this->sut->admin_scripts();
+
+ $quick_edit = wp_scripts()->registered['woocommerce_quick-edit'];
+
+ $this->assertNotContains( 'jquery-ui-datepicker', $quick_edit->deps, 'Quick Edit should load even when another plugin deregisters the datepicker.' );
+ $this->assertTrue( wp_script_is( 'jquery-ui-datepicker', 'enqueued' ), 'The datepicker should still be requested when it is available.' );
+ }
+
/**
* @testdox Should set up the lost connection notice and heartbeat correctly per screen, and never re-enqueue autosave.
* @testWith ["woocommerce_page_wc-orders", "woocommerce_page_wc-orders", "", false, true]
diff --git a/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-post-types-test.php b/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-post-types-test.php
index a9220c36d20..32f543cd38d 100644
--- a/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-post-types-test.php
+++ b/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-post-types-test.php
@@ -116,6 +116,7 @@ class WC_Admin_Post_Types_Test extends WC_Unit_Test_Case {
'active simple schedule' => array( ProductType::SIMPLE, '2000-01-01 00:00:00', '2099-12-31 23:59:59', '2000-01-01', '2099-12-31', '2000-01-01 00:00:00', '2099-12-31 23:59:59' ),
'future external schedule' => array( ProductType::EXTERNAL, '2098-01-01 00:00:00', '2098-12-31 23:59:59', '2098-02-01', '2098-11-30', '2098-02-01 00:00:00', '2098-11-30 23:59:59' ),
'expired simple schedule' => array( ProductType::SIMPLE, '2000-01-01 00:00:00', '2001-01-01 23:59:59', '2000-01-01', '2001-01-01', '2000-01-01 00:00:00', '2001-01-01 23:59:59' ),
+ 'valid leap day schedule' => array( ProductType::SIMPLE, null, null, '2024-02-29', '2024-02-29', '2024-02-29 00:00:00', '2024-02-29 23:59:59' ),
'empty external schedule' => array( ProductType::EXTERNAL, null, null, '', '', null, null ),
);
}
@@ -196,24 +197,48 @@ class WC_Admin_Post_Types_Test extends WC_Unit_Test_Case {
}
/**
- * @testdox Quick Edit normalizes invalid sale dates like the full product editor.
+ * @testdox Quick Edit preserves sale dates when submitted values are invalid.
+ * @dataProvider invalid_sale_date_provider
+ *
+ * @param mixed $submitted_date Submitted sale date.
*/
- public function test_quick_edit_matches_full_editor_invalid_date_behavior(): void {
+ public function test_quick_edit_preserves_sale_dates_for_invalid_values( $submitted_date ): void {
$product = $this->create_product( ProductType::SIMPLE );
+ $product->set_date_on_sale_from( '2098-01-01 00:00:00' );
+ $product->set_date_on_sale_to( '2098-12-31 23:59:59' );
+ $product->save();
$this->quick_edit(
$product,
array(
- '_sale_price_dates_from' => 'invalid-date',
- '_sale_price_dates_to' => 'invalid-date',
+ '_sale_price_dates_from' => $submitted_date,
+ '_sale_price_dates_to' => $submitted_date,
)
);
$updated_product = wc_get_product( $product->get_id() );
- // Like the full editor, an invalid start becomes timestamp 0, which WC_Data treats as empty on reload.
- $this->assert_date( null, $updated_product->get_date_on_sale_from( 'edit' ), 'start' );
- $this->assert_date( '1970-01-01 23:59:59', $updated_product->get_date_on_sale_to( 'edit' ), 'end' );
+ $this->assert_date( '2098-01-01 00:00:00', $updated_product->get_date_on_sale_from( 'edit' ), 'start' );
+ $this->assert_date( '2098-12-31 23:59:59', $updated_product->get_date_on_sale_to( 'edit' ), 'end' );
+ }
+
+ /**
+ * Provides invalid sale dates.
+ *
+ * @return array<string, array{mixed}>
+ */
+ public static function invalid_sale_date_provider(): array {
+ return array(
+ 'unparseable string' => array( 'tomorow' ),
+ 'non-string value' => array( array( '2025-02-28' ) ),
+ 'invalid leap day' => array( '2025-02-29' ),
+ 'invalid June day' => array( '2025-06-31' ),
+ 'zero month' => array( '2025-00-30' ),
+ 'month without zero' => array( '2025-2-03' ),
+ 'time suffix' => array( '2025-02-03 00:00:00' ),
+ 'whitespace only' => array( ' ' ),
+ 'padded date' => array( ' 2025-02-03 ' ),
+ );
}
/**