Commit 45bb71760a4 for woocommerce
commit 45bb71760a4790cafa0ac6d28e2b134a3ca50be8
Author: Luigi Teschio <gigitux@gmail.com>
Date: Mon Jun 29 11:33:45 2026 +0200
Fix variation bulk sale schedule dates (#66013)
* Fix: validate end-date >= start-date in variation bulk sale-schedule action
The "Set scheduled sale dates" bulk action on a variable product's
Variations tab used two raw window.prompt() calls with no cross-field
validation. A user could enter an end date earlier than the start
date, which silently saved an empty sale window (variation never on
sale).
Add a client-side check after the second prompt that parses both
values with Date.parse() and aborts the bulk action with an alert
when end < start, matching the constraint the single-variation
datepicker already enforces via minDate/maxDate.
Fixes #64634
* Fix variation bulk sale schedule dates
* Update changelog for variation bulk sale dates
* clean up test
* improve logic
* use const
---------
Co-authored-by: Samrat Biswas <samrat.biswas@automattic.com>
diff --git a/plugins/woocommerce/changelog/64634-fix-variation-bulk-sale-schedule-end-before-start b/plugins/woocommerce/changelog/64634-fix-variation-bulk-sale-schedule-end-before-start
new file mode 100644
index 00000000000..1382cab6848
--- /dev/null
+++ b/plugins/woocommerce/changelog/64634-fix-variation-bulk-sale-schedule-end-before-start
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Variation bulk action "Set scheduled sale dates": block submission when the sale end date is earlier than the sale start date, and clear sale dates submitted as blank instead of storing 1970-01-01.
diff --git a/plugins/woocommerce/client/legacy/js/admin/meta-boxes-product-variation.js b/plugins/woocommerce/client/legacy/js/admin/meta-boxes-product-variation.js
index 9a04122d4d0..1d37ce9e4c5 100644
--- a/plugins/woocommerce/client/legacy/js/admin/meta-boxes-product-variation.js
+++ b/plugins/woocommerce/client/legacy/js/admin/meta-boxes-product-variation.js
@@ -1336,7 +1336,6 @@ jQuery( function ( $ ) {
data.date_to = window.prompt(
woocommerce_admin_meta_boxes_variations.i18n_scheduled_sale_end
);
-
if ( null === data.date_from ) {
data.date_from = false;
}
@@ -1347,6 +1346,20 @@ jQuery( function ( $ ) {
if ( false === data.date_to && false === data.date_from ) {
cancel = true;
+ } else if ( data.date_from && data.date_to ) {
+ const from_ts = Date.parse( data.date_from );
+ const to_ts = Date.parse( data.date_to );
+
+ if (
+ ! isNaN( from_ts ) &&
+ ! isNaN( to_ts ) &&
+ to_ts < from_ts
+ ) {
+ window.alert(
+ woocommerce_admin_meta_boxes_variations.i18n_scheduled_sale_end_before_start
+ );
+ cancel = true;
+ }
}
break;
default:
diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-assets.php b/plugins/woocommerce/includes/admin/class-wc-admin-assets.php
index 88ca40adecf..d627054ad50 100644
--- a/plugins/woocommerce/includes/admin/class-wc-admin-assets.php
+++ b/plugins/woocommerce/includes/admin/class-wc-admin-assets.php
@@ -582,6 +582,7 @@ if ( ! class_exists( 'WC_Admin_Assets', false ) ) :
'i18n_remove_variation' => esc_js( __( 'Are you sure you want to remove this variation?', 'woocommerce' ) ),
'i18n_scheduled_sale_start' => esc_js( __( 'Sale start date (YYYY-MM-DD format or leave blank)', 'woocommerce' ) ),
'i18n_scheduled_sale_end' => esc_js( __( 'Sale end date (YYYY-MM-DD format or leave blank)', 'woocommerce' ) ),
+ 'i18n_scheduled_sale_end_before_start' => esc_js( __( 'The sale end date cannot be earlier than the sale start date.', 'woocommerce' ) ),
'i18n_edited_variations' => esc_js( __( 'Save changes before changing page?', 'woocommerce' ) ),
'i18n_variation_count_single' => esc_js( __( '1 variation', 'woocommerce' ) ),
'i18n_variation_count_plural' => esc_js( __( '%qty% variations', 'woocommerce' ) ),
diff --git a/plugins/woocommerce/includes/class-wc-ajax.php b/plugins/woocommerce/includes/class-wc-ajax.php
index 4dea2ad4f33..599c9d2a7ce 100644
--- a/plugins/woocommerce/includes/class-wc-ajax.php
+++ b/plugins/woocommerce/includes/class-wc-ajax.php
@@ -3010,16 +3010,19 @@ class WC_AJAX {
return;
}
+ $date_from = isset( $data['date_from'] ) ? wc_clean( $data['date_from'] ) : false;
+ $date_to = isset( $data['date_to'] ) ? wc_clean( $data['date_to'] ) : false;
+
foreach ( $variations as $variation_id ) {
$variation = wc_get_product( $variation_id );
- if ( 'false' !== $data['date_from'] ) {
- $date_on_sale_from = date( 'Y-m-d 00:00:00', strtotime( wc_clean( $data['date_from'] ) ) ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
+ if ( false !== $date_from && 'false' !== $date_from ) {
+ $date_on_sale_from = '' === $date_from ? null : date( 'Y-m-d 00:00:00', strtotime( $date_from ) ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
$variation->set_date_on_sale_from( $date_on_sale_from );
}
- if ( 'false' !== $data['date_to'] ) {
- $date_on_sale_to = date( 'Y-m-d 23:59:59', strtotime( wc_clean( $data['date_to'] ) ) ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
+ if ( false !== $date_to && 'false' !== $date_to ) {
+ $date_on_sale_to = '' === $date_to ? null : date( 'Y-m-d 23:59:59', strtotime( $date_to ) ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
$variation->set_date_on_sale_to( $date_on_sale_to );
}
diff --git a/plugins/woocommerce/tests/php/includes/class-wc-ajax-test.php b/plugins/woocommerce/tests/php/includes/class-wc-ajax-test.php
index d365b6e6ab1..414bdbe8193 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-ajax-test.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-ajax-test.php
@@ -606,6 +606,38 @@ class WC_AJAX_Test extends \WP_Ajax_UnitTestCase {
$product->delete( true );
}
+ /**
+ * @testdox Should clear variation sale dates when bulk schedule dates are blank.
+ * @group ajax
+ */
+ public function test_bulk_sale_schedule_clears_blank_dates(): void {
+ $variation = new WC_Product_Variation();
+ $variation->set_date_on_sale_from( '2026-06-01 00:00:00' );
+ $variation->set_date_on_sale_to( '2026-06-30 23:59:59' );
+ $variation->save();
+
+ $method = new ReflectionMethod( WC_AJAX::class, 'variation_bulk_action_variable_sale_schedule' );
+ $method->setAccessible( true );
+
+ $method->invokeArgs(
+ null,
+ array(
+ array( $variation->get_id() ),
+ array(
+ 'date_from' => '',
+ 'date_to' => '',
+ ),
+ )
+ );
+
+ $variation = wc_get_product( $variation->get_id() );
+
+ $this->assertNull( $variation->get_date_on_sale_from( 'edit' ), 'The sale start date should be cleared when the bulk action start date is blank.' );
+ $this->assertNull( $variation->get_date_on_sale_to( 'edit' ), 'The sale end date should be cleared when the bulk action end date is blank.' );
+
+ $variation->delete( true );
+ }
+
/**
* Does the 'hard work' of triggering an ajax endpoint and capturing the response.
*