From 0ebc6863e379e2816e46e3121ddcd623d6298526 Mon Sep 17 00:00:00 2001 From: Sukhendu Sekhar Guria Date: Wed, 3 Jun 2026 13:36:05 +0530 Subject: [PATCH 1/2] Fix add_option duplicate key behavior --- src/wp-includes/option.php | 11 ++++++++- tests/phpunit/tests/option/option.php | 32 +++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/option.php b/src/wp-includes/option.php index 7979c119a986f..e280da313b772 100644 --- a/src/wp-includes/option.php +++ b/src/wp-includes/option.php @@ -1137,8 +1137,17 @@ function add_option( $option, $value = '', $deprecated = '', $autoload = null ) */ do_action( 'add_option', $option, $value ); - $result = $wpdb->query( $wpdb->prepare( "INSERT INTO `$wpdb->options` (`option_name`, `option_value`, `autoload`) VALUES (%s, %s, %s) ON DUPLICATE KEY UPDATE `option_name` = VALUES(`option_name`), `option_value` = VALUES(`option_value`), `autoload` = VALUES(`autoload`)", $option, $serialized_value, $autoload ) ); + $result = $wpdb->query( $wpdb->prepare( "INSERT INTO `$wpdb->options` (`option_name`, `option_value`, `autoload`) VALUES (%s, %s, %s) ON DUPLICATE KEY UPDATE `option_name` = `option_name`", $option, $serialized_value, $autoload ) ); if ( ! $result ) { + if ( 0 === $result && ! wp_installing() ) { + $notoptions = wp_cache_get( 'notoptions', 'options' ); + + if ( is_array( $notoptions ) && isset( $notoptions[ $option ] ) ) { + unset( $notoptions[ $option ] ); + wp_cache_set( 'notoptions', $notoptions, 'options' ); + } + } + return false; } diff --git a/tests/phpunit/tests/option/option.php b/tests/phpunit/tests/option/option.php index 53cc5e75f4d31..6ff99566366a1 100644 --- a/tests/phpunit/tests/option/option.php +++ b/tests/phpunit/tests/option/option.php @@ -40,6 +40,38 @@ public function test_the_basics() { $this->assertFalse( get_option( $key2 ) ); } + /** + * @ticket 51486 + * + * @covers ::add_option + */ + public function test_add_option_should_not_update_existing_option_when_notoptions_cache_is_stale() { + global $wpdb; + + $option = 'ticket_51486'; + + $this->assertTrue( add_option( $option, 'original', '', false ) ); + + $notoptions = wp_cache_get( 'notoptions', 'options' ); + $notoptions = is_array( $notoptions ) ? $notoptions : array(); + $notoptions[ $option ] = true; + wp_cache_set( 'notoptions', $notoptions, 'options' ); + + $this->assertFalse( add_option( $option, 'changed', '', true ) ); + + $row = $wpdb->get_row( $wpdb->prepare( "SELECT option_value, autoload FROM $wpdb->options WHERE option_name = %s", $option ) ); + + $this->assertSame( 'original', $row->option_value ); + $this->assertSame( 'off', $row->autoload ); + + wp_cache_delete( $option, 'options' ); + $this->assertSame( 'original', get_option( $option ) ); + + $notoptions = wp_cache_get( 'notoptions', 'options' ); + $this->assertIsArray( $notoptions ); + $this->assertArrayNotHasKey( $option, $notoptions ); + } + /** * @covers ::get_option * @covers ::add_option From 72ae859a439e56dbff30ded31159a28f98c4932f Mon Sep 17 00:00:00 2001 From: Sukhendu Sekhar Guria Date: Wed, 3 Jun 2026 14:44:49 +0530 Subject: [PATCH 2/2] Fix update_option() fallback for false values --- src/wp-includes/option.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/option.php b/src/wp-includes/option.php index e280da313b772..79be58243ae27 100644 --- a/src/wp-includes/option.php +++ b/src/wp-includes/option.php @@ -924,7 +924,13 @@ function update_option( $option, $value, $autoload = null ) { /** This filter is documented in wp-includes/option.php */ if ( apply_filters( "default_option_{$option}", false, $option, false ) === $old_value ) { - return add_option( $option, $value, '', $autoload ); + if ( add_option( $option, $value, '', $autoload ) ) { + return true; + } + + if ( ! $wpdb->get_var( $wpdb->prepare( "SELECT option_id FROM $wpdb->options WHERE option_name = %s LIMIT 1", $option ) ) ) { + return false; + } } $serialized_value = maybe_serialize( $value );