From bb5d25c0cf8f7a7e7eac88d8cd4c675003aec2a2 Mon Sep 17 00:00:00 2001 From: Sainath Poojary Date: Thu, 10 Jul 2025 15:27:08 +0530 Subject: [PATCH 1/2] Options: Add `pre-add-option` filter --- src/wp-includes/option.php | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/wp-includes/option.php b/src/wp-includes/option.php index 3b4f461724fad..d9941d7ac67f2 100644 --- a/src/wp-includes/option.php +++ b/src/wp-includes/option.php @@ -1113,6 +1113,23 @@ function add_option( $option, $value = '', $deprecated = '', $autoload = null ) $value = sanitize_option( $option, $value ); + /** + * Filters whether to short-circuit the process of adding an option. + * + * Returning a value other than null will short-circuit the process, and the value will be + * returned cast as a bool. + * + * @since 4.7.0 + * + * @param null $skip Whether to short-circuit the process. Default null. + * @param string $option Name of the option to add. + * @param mixed $value Option value. + */ + $skip = apply_filters( 'pre_add_option', null, $option, $value ); + if ( ! is_null( $skip ) ) { + return (bool) $skip; + } + /* * Make sure the option doesn't already exist. * We can check the 'notoptions' cache before we ask for a DB query. From ba75de07119f81c6764a201de116a72bc9d567ee Mon Sep 17 00:00:00 2001 From: Sainath Poojary Date: Wed, 19 Aug 2026 00:24:30 +0530 Subject: [PATCH 2/2] feat: add dynamic pre_add_option_{$option} filter to allow short-circuiting specific options --- src/wp-includes/option.php | 25 ++++++++++-- tests/phpunit/tests/option/option.php | 55 +++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 4 deletions(-) diff --git a/src/wp-includes/option.php b/src/wp-includes/option.php index d9941d7ac67f2..794eda4250eb9 100644 --- a/src/wp-includes/option.php +++ b/src/wp-includes/option.php @@ -1113,19 +1113,36 @@ function add_option( $option, $value = '', $deprecated = '', $autoload = null ) $value = sanitize_option( $option, $value ); + /** + * Filters whether to short-circuit adding a specific option. + * + * The dynamic portion of the hook name, `$option`, refers to the option name. + * + * Returning a non-null value will short-circuit adding the option, and the + * returned value will be used as a boolean success/failure return value. + * + * @since 7.2.0 + * + * @param null $skip Whether to short-circuit adding the option. Default null. + * @param string $option Name of the option to add. + * @param mixed $value Option value. + */ + $skip = apply_filters( "pre_add_option_{$option}", null, $option, $value ); + /** * Filters whether to short-circuit the process of adding an option. * - * Returning a value other than null will short-circuit the process, and the value will be - * returned cast as a bool. + * Returning a non-null value will short-circuit adding the option, and the + * returned value will be used as a boolean success/failure return value. * - * @since 4.7.0 + * @since 7.2.0 * * @param null $skip Whether to short-circuit the process. Default null. * @param string $option Name of the option to add. * @param mixed $value Option value. */ - $skip = apply_filters( 'pre_add_option', null, $option, $value ); + $skip = apply_filters( 'pre_add_option', $skip, $option, $value ); + if ( ! is_null( $skip ) ) { return (bool) $skip; } diff --git a/tests/phpunit/tests/option/option.php b/tests/phpunit/tests/option/option.php index 9a2f061aa369c..41b14d8f01f4f 100644 --- a/tests/phpunit/tests/option/option.php +++ b/tests/phpunit/tests/option/option.php @@ -613,4 +613,59 @@ public function helper_object_cache_stats_cmd_get() { return $stats['cmd_get']; } + + /** + * Data provider for test_pre_add_option_filter_short_circuits(). + * + * @return array[] + */ + public function data_pre_add_option_filter_short_circuits() { + return array( + 'returns true' => array( '__return_true', true ), + 'returns false' => array( '__return_false', false ), + ); + } + + /** + * @ticket 37928 + * + * @dataProvider data_pre_add_option_filter_short_circuits + * + * @covers ::add_option + * + * @param callable $callback The filter callback to use. + * @param bool $expected The expected return value of add_option(). + */ + public function test_pre_add_option_filter_short_circuits( $callback, $expected ) { + add_filter( 'pre_add_option', $callback ); + $result = add_option( 'test_pre_add_option', 'value' ); + remove_filter( 'pre_add_option', $callback ); + + $this->assertSame( $expected, $result ); + // Confirm the option was NOT actually written to the database. + $this->assertFalse( get_option( 'test_pre_add_option' ) ); + } + + /** + * @ticket 37928 + * + * @covers ::add_option + */ + public function test_pre_add_option_dynamic_filter_short_circuits_specific_option() { + add_filter( 'pre_add_option_test_specific_option', '__return_true' ); + $result_specific = add_option( 'test_specific_option', 'value' ); + + // A different option must NOT be short-circuited by the per-option filter. + $result_other = add_option( 'test_other_option', 'value' ); + + remove_filter( 'pre_add_option_test_specific_option', '__return_true' ); + + // The targeted option was short-circuited. + $this->assertTrue( $result_specific ); + $this->assertFalse( get_option( 'test_specific_option' ) ); + + // The other option was written normally. + $this->assertTrue( $result_other ); + $this->assertSame( 'value', get_option( 'test_other_option' ) ); + } }