Commit eca90038e50 for woocommerce
commit eca90038e509a2bc05307457e1f4045d9fd3559c
Author: Chris Lilitsas <1105590+xristos3490@users.noreply.github.com>
Date: Thu Oct 15 21:02:24 2026 +0300
Reject negative values for the unverified stock notification deletion threshold (#69505)
* fix: treat negative BIS unverified sign-up deletion threshold as off
* fix: unschedule BIS daily cleanup when the stored threshold is negative
* fix: render the clamped BIS unverified deletion threshold so a stored negative value does not block saving
With min="0" on the field, a stored negative value rendered as-is and failed browser validation. When double opt-in is off the row is hidden, so the form could not be submitted and no message was shown. Pass the clamped Config value as the field value.
diff --git a/plugins/woocommerce/changelog/fix-wooplug-7926-bis-threshold-negative b/plugins/woocommerce/changelog/fix-wooplug-7926-bis-threshold-negative
new file mode 100644
index 00000000000..8af2f69ba3b
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-wooplug-7926-bis-threshold-negative
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Save a negative "Delete unverified notification sign-ups after" value as 0, which turns the cleanup off.
diff --git a/plugins/woocommerce/src/Internal/StockNotifications/Admin/SettingsController.php b/plugins/woocommerce/src/Internal/StockNotifications/Admin/SettingsController.php
index 204957fe046..25ad5c412fd 100644
--- a/plugins/woocommerce/src/Internal/StockNotifications/Admin/SettingsController.php
+++ b/plugins/woocommerce/src/Internal/StockNotifications/Admin/SettingsController.php
@@ -26,6 +26,8 @@ class SettingsController {
// Add the My Account endpoint setting to the Advanced tab.
add_filter( 'woocommerce_get_settings_advanced', array( $this, 'add_my_account_endpoint_setting' ), 100, 2 );
+ add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_customer_stock_notifications_unverified_deletions_days_threshold', array( $this, 'sanitize_unverified_deletion_days_threshold' ) );
+
// The product edit hooks stay admin-only: process_product_object() reads $_POST and
// checks an admin nonce, so it must not run for cron or CLI code that fires the hook.
if ( ! is_admin() ) {
@@ -174,11 +176,16 @@ class SettingsController {
),
array(
- 'title' => __( 'Delete unverified notification sign-ups after (in days)', 'woocommerce' ),
- 'desc' => __( 'Controls how long unverified notification sign-ups are stored in the database. Enter zero, or leave this field empty, to store unverified sign-up requests indefinitely.', 'woocommerce' ),
- 'id' => 'woocommerce_customer_stock_notifications_unverified_deletions_days_threshold',
- 'default' => Config::get_unverified_deletion_days_threshold(),
- 'type' => 'number',
+ 'title' => __( 'Delete unverified notification sign-ups after (in days)', 'woocommerce' ),
+ 'desc' => __( 'Controls how long unverified notification sign-ups are stored in the database. Enter zero, or leave this field empty, to store unverified sign-up requests indefinitely.', 'woocommerce' ),
+ 'id' => 'woocommerce_customer_stock_notifications_unverified_deletions_days_threshold',
+ 'default' => Config::get_unverified_deletion_days_threshold(),
+ 'value' => Config::get_unverified_deletion_days_threshold(),
+ 'type' => 'number',
+ 'custom_attributes' => array(
+ 'min' => 0,
+ 'step' => 1,
+ ),
),
array(
@@ -202,6 +209,26 @@ class SettingsController {
return $settings;
}
+ /**
+ * Store negative values of the unverified sign-up deletion threshold as 0, which turns the cleanup off.
+ *
+ * An empty value is kept as is, since it also means off.
+ *
+ * @internal
+ *
+ * @since 11.3.0
+ *
+ * @param mixed $value The value about to be saved.
+ * @return mixed
+ */
+ public function sanitize_unverified_deletion_days_threshold( $value ) {
+ if ( null === $value || '' === $value ) {
+ return $value;
+ }
+
+ return max( 0, (int) $value );
+ }
+
/**
* Display admin notices about incompatible settings combinations.
*
diff --git a/plugins/woocommerce/src/Internal/StockNotifications/Config.php b/plugins/woocommerce/src/Internal/StockNotifications/Config.php
index 978c9f81d44..d1f839254f0 100644
--- a/plugins/woocommerce/src/Internal/StockNotifications/Config.php
+++ b/plugins/woocommerce/src/Internal/StockNotifications/Config.php
@@ -181,8 +181,9 @@ class Config {
* @return int
*/
public static function get_unverified_deletion_days_threshold(): int {
- return absint(
- get_option(
+ return max(
+ 0,
+ (int) get_option(
'woocommerce_customer_stock_notifications_unverified_deletions_days_threshold',
0
)
diff --git a/plugins/woocommerce/src/Internal/StockNotifications/DataRetentionController.php b/plugins/woocommerce/src/Internal/StockNotifications/DataRetentionController.php
index 8348f2ec82b..caa3d1c2d60 100644
--- a/plugins/woocommerce/src/Internal/StockNotifications/DataRetentionController.php
+++ b/plugins/woocommerce/src/Internal/StockNotifications/DataRetentionController.php
@@ -38,14 +38,14 @@ class DataRetentionController {
/**
* Responds to changes in the option for deleting unverified notifications.
* If the new value is numeric and greater than zero, it schedules a daily task.
- * If the new value is not numeric or is empty, it clears the scheduled tasks.
+ * If the new value is not numeric, or is zero or less, it clears the scheduled tasks.
*
* @param mixed $unused The old option value or option name (not used in this function).
* @param mixed $new_option_value The new value of the option.
* @return void
*/
public function schedule_or_unschedule_daily_task( $unused, $new_option_value ): void {
- if ( ! is_numeric( $new_option_value ) || empty( $new_option_value ) ) {
+ if ( ! is_numeric( $new_option_value ) || (int) $new_option_value <= 0 ) {
$this->clear_daily_task();
return;
}
diff --git a/plugins/woocommerce/tests/php/src/Internal/StockNotifications/Admin/SettingsControllerTests.php b/plugins/woocommerce/tests/php/src/Internal/StockNotifications/Admin/SettingsControllerTests.php
index 053f3752124..f66062f9712 100644
--- a/plugins/woocommerce/tests/php/src/Internal/StockNotifications/Admin/SettingsControllerTests.php
+++ b/plugins/woocommerce/tests/php/src/Internal/StockNotifications/Admin/SettingsControllerTests.php
@@ -5,6 +5,7 @@ namespace Automattic\WooCommerce\Tests\Internal\StockNotifications\Admin;
use Automattic\WooCommerce\Internal\StockNotifications\Admin\SettingsController;
use Automattic\WooCommerce\Internal\StockNotifications\Config;
+use Automattic\WooCommerce\Internal\StockNotifications\DataRetentionController;
use Automattic\WooCommerce\Internal\StockNotifications\Frontend\MyAccountEndpoint;
use WC_Helper_Product;
use WC_Settings_Advanced;
@@ -86,6 +87,7 @@ class SettingsControllerTests extends \WC_Settings_Unit_Test_Case {
$this->assertSame( 100, has_filter( 'woocommerce_get_sections_products', array( $sut, 'add_customer_stock_notifications_section' ) ) );
$this->assertSame( 100, has_filter( 'woocommerce_get_settings_products', array( $sut, 'add_customer_stock_notifications_settings' ) ) );
$this->assertSame( 100, has_filter( 'woocommerce_get_settings_advanced', array( $sut, 'add_my_account_endpoint_setting' ) ) );
+ $this->assertSame( 10, has_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_customer_stock_notifications_unverified_deletions_days_threshold', array( $sut, 'sanitize_unverified_deletion_days_threshold' ) ) );
$this->assertFalse( has_action( 'admin_notices', array( $sut, 'output_admin_notices' ) ) );
$this->assertFalse( has_action( 'woocommerce_product_options_stock_status', array( $sut, 'add_disable_stock_notifications_checkbox' ) ) );
$this->assertFalse( has_action( 'woocommerce_admin_process_product_object', array( $sut, 'process_product_object' ) ) );
@@ -226,6 +228,47 @@ class SettingsControllerTests extends \WC_Settings_Unit_Test_Case {
$this->assertSame( 'yes', $product->get_meta( $meta_key ) );
}
+ /**
+ * @testdox Saving a negative unverified sign-up deletion threshold stores 0 and unschedules the cleanup.
+ */
+ public function test_negative_unverified_deletion_days_threshold_is_saved_as_zero() {
+ $option_id = 'woocommerce_customer_stock_notifications_unverified_deletions_days_threshold';
+ $retention_controller = new DataRetentionController();
+ update_option( $option_id, 30 );
+ $this->assertSame( 'daily', wp_get_schedule( DataRetentionController::DAILY_TASK_HOOK ), 'A positive threshold should schedule the cleanup.' );
+
+ \WC_Admin_Settings::save_fields(
+ array(
+ array(
+ 'id' => $option_id,
+ 'type' => 'number',
+ ),
+ ),
+ array( $option_id => '-5' )
+ );
+
+ $this->assertSame( 0, (int) get_option( $option_id ), 'A negative threshold should be stored as 0.' );
+ $this->assertFalse( wp_get_schedule( DataRetentionController::DAILY_TASK_HOOK ), 'A negative threshold should turn the cleanup off.' );
+
+ $retention_controller->clear_daily_task();
+ }
+
+ /**
+ * @testdox A stored negative unverified sign-up deletion threshold renders as 0 so the settings form stays submittable.
+ */
+ public function test_stored_negative_unverified_deletion_days_threshold_renders_as_zero() {
+ $option_id = 'woocommerce_customer_stock_notifications_unverified_deletions_days_threshold';
+ update_option( $option_id, -5 );
+
+ $settings = ( new WC_Settings_Products() )->get_settings_for_section( 'customer_stock_notifications' );
+ $field = current( array_filter( $settings, fn( $setting ) => ( $setting['id'] ?? '' ) === $option_id ) );
+
+ $this->assertSame( 0, $field['value'] );
+ $this->assertSame( 0, $field['custom_attributes']['min'] );
+
+ delete_option( $option_id );
+ }
+
/**
* Reset the REST server and request globals so this test does not leak them into tests that run after it.
*/
diff --git a/plugins/woocommerce/tests/php/src/Internal/StockNotifications/DataRetentionControllerTests.php b/plugins/woocommerce/tests/php/src/Internal/StockNotifications/DataRetentionControllerTests.php
index 48185282de8..cc806a5e129 100644
--- a/plugins/woocommerce/tests/php/src/Internal/StockNotifications/DataRetentionControllerTests.php
+++ b/plugins/woocommerce/tests/php/src/Internal/StockNotifications/DataRetentionControllerTests.php
@@ -3,6 +3,7 @@
declare( strict_types = 1 );
namespace Automattic\WooCommerce\Tests\Internal\StockNotifications;
+use Automattic\WooCommerce\Internal\StockNotifications\Config;
use Automattic\WooCommerce\Internal\StockNotifications\DataRetentionController;
use Automattic\WooCommerce\Internal\StockNotifications\Enums\NotificationStatus;
use Automattic\WooCommerce\Internal\StockNotifications\Frontend\NotificationManagementService;
@@ -81,6 +82,19 @@ class DataRetentionControllerTests extends \WC_Unit_Test_Case {
$this->assertFalse( $schedule );
}
+ /**
+ * Test that an already stored negative threshold turns the cleanup off and unschedules the daily task.
+ */
+ public function test_negative_threshold_is_treated_as_zero() {
+ update_option( 'woocommerce_customer_stock_notifications_unverified_deletions_days_threshold', 30 );
+ $this->assertEquals( 'daily', wp_get_schedule( DataRetentionController::DAILY_TASK_HOOK ) );
+
+ update_option( 'woocommerce_customer_stock_notifications_unverified_deletions_days_threshold', '-5' );
+
+ $this->assertSame( 0, Config::get_unverified_deletion_days_threshold() );
+ $this->assertFalse( wp_get_schedule( DataRetentionController::DAILY_TASK_HOOK ) );
+ }
+
/**
* Test that the daily task deletes expired notifications.
*