From 7270890167fa34c6aae218b72218302df9baabb9 Mon Sep 17 00:00:00 2001 From: Faisal Ahammad Date: Sun, 19 Jul 2026 18:23:31 +0600 Subject: [PATCH] fix(settings): keep wp_stream option autoload explicit - Pass true on update_option when writing per-site settings - Register setting with autoload true for the Settings API path - Repair stray no-autoload rows on admin_init via wp_set_option_autoload - Add unit tests for write path, repair, idempotency, and network no-op Settings load on every request for exclude rules, logging gates, and role access, so autoload stays on by design. Fixes #1482 --- classes/class-settings.php | 41 +++++- tests/phpunit/test-class-settings.php | 177 ++++++++++++++++++++++++++ 2 files changed, 215 insertions(+), 3 deletions(-) create mode 100644 tests/phpunit/test-class-settings.php diff --git a/classes/class-settings.php b/classes/class-settings.php index d2ade1917..f90773d95 100644 --- a/classes/class-settings.php +++ b/classes/class-settings.php @@ -65,6 +65,11 @@ public function __construct( $plugin ) { // Register settings, and fields. add_action( 'admin_init', array( $this, 'register_settings' ) ); + // One-shot repair for installs whose wp_stream row was flipped off + // autoload. admin_init is enough: default writes already use yes, and + // a single admin hit after upgrade fixes any stray no. See #1482. + add_action( 'admin_init', array( $this, 'ensure_option_autoload' ) ); + // Remove records when records TTL is shortened. add_action( 'update_option_' . $this->option_key, @@ -577,7 +582,10 @@ public function update_all_setting_values( array $options ) { if ( $is_network ) { $result = update_site_option( $this->network_options_key, $options ); } else { - $result = update_option( $this->option_key, $options ); + // Autoload on purpose: Settings loads this option on every request + // (Plugin::init -> Settings::__construct -> get_options) for exclude + // rules, logging gates, and role access. See issue #1482. + $result = update_option( $this->option_key, $options, true ); } // Refresh the in-memory copy so subsequent reads in the same request @@ -600,6 +608,31 @@ public function update_all_setting_values( array $options ) { return (bool) $result; } + /** + * Ensure the per-site settings option stays autoloaded. + * + * Settings are read on every request via get_options(), so autoload=yes + * avoids an extra query. Idempotent. No-op for the network option key + * (sitemeta has no autoload flag) and when wp_set_option_autoload() is + * unavailable (WordPress < 6.4). + * + * @see https://github.com/xwp/stream/issues/1482 + * + * @return void + */ + public function ensure_option_autoload() { + if ( ! function_exists( 'wp_set_option_autoload' ) ) { + return; + } + + // Network settings live in sitemeta; no autoload flag to maintain. + if ( $this->option_key === $this->network_options_key ) { + return; + } + + wp_set_option_autoload( $this->option_key, true ); + } + /** * Returns a list of options based on the current screen. * @@ -695,8 +728,10 @@ public function register_settings() { $this->option_key, $this->option_key, array( - $this, - 'sanitize_settings', + 'type' => 'array', + 'sanitize_callback' => array( $this, 'sanitize_settings' ), + // Autoload on purpose — see ensure_option_autoload() / issue #1482. + 'autoload' => true, ) ); diff --git a/tests/phpunit/test-class-settings.php b/tests/phpunit/test-class-settings.php new file mode 100644 index 000000000..48802e8a1 --- /dev/null +++ b/tests/phpunit/test-class-settings.php @@ -0,0 +1,177 @@ +get_var( + $wpdb->prepare( + "SELECT autoload FROM {$wpdb->options} WHERE option_name = %s LIMIT 1", + $option_name + ) + ); + + return null === $value ? null : (string) $value; + } + + /** + * Whether the current run is a network-activated multisite install. + * + * The update_all_setting_values() method writes to sitemeta in that case, + * which has no autoload column to assert against. + * + * @return bool + */ + private function is_network_settings_store() { + return is_multisite() && $this->plugin->is_network_activated(); + } + + public function test_update_all_setting_values_passes_autoload_true() { + if ( $this->is_network_settings_store() ) { + $this->markTestSkipped( 'Network-activated installs write sitemeta, not wp_options.autoload.' ); + } + + $option_key = $this->plugin->settings->option_key; + + $result = $this->plugin->settings->update_all_setting_values( + array( + 'general_records_ttl' => 42, + ) + ); + + $this->assertTrue( $result ); + + $stored = get_option( $option_key ); + $this->assertTrue( is_array( $stored ) ); + $this->assertSame( 42, (int) $stored['general_records_ttl'] ); + + $autoload = $this->get_option_autoload( $option_key ); + $this->assertNotNull( $autoload ); + $this->assertContains( + $autoload, + self::AUTOLOAD_YES_FAMILY, + 'wp_stream must stay autoloaded after update_all_setting_values().' + ); + } + + public function test_ensure_option_autoload_sets_yes_when_no() { + if ( ! function_exists( 'wp_set_option_autoload' ) ) { + $this->markTestSkipped( 'wp_set_option_autoload() requires WordPress 6.4+.' ); + } + + if ( $this->is_network_settings_store() ) { + $this->markTestSkipped( 'Network-activated installs write sitemeta, not wp_options.autoload.' ); + } + + $option_key = $this->plugin->settings->option_key; + + delete_option( $option_key ); + add_option( $option_key, array( 'general_records_ttl' => 30 ), '', 'no' ); + + $before = $this->get_option_autoload( $option_key ); + $this->assertContains( + $before, + self::AUTOLOAD_NO_FAMILY, + 'Precondition: option must start as not-autoloaded.' + ); + + $this->plugin->settings->ensure_option_autoload(); + + $after = $this->get_option_autoload( $option_key ); + $this->assertContains( + $after, + self::AUTOLOAD_YES_FAMILY, + 'ensure_option_autoload() must flip a no-autoload row to yes-family.' + ); + } + + public function test_ensure_option_autoload_is_idempotent() { + if ( ! function_exists( 'wp_set_option_autoload' ) ) { + $this->markTestSkipped( 'wp_set_option_autoload() requires WordPress 6.4+.' ); + } + + if ( $this->is_network_settings_store() ) { + $this->markTestSkipped( 'Network-activated installs write sitemeta, not wp_options.autoload.' ); + } + + $option_key = $this->plugin->settings->option_key; + + // Ensure a yes-family row exists. + delete_option( $option_key ); + add_option( $option_key, array( 'general_records_ttl' => 30 ), '', 'yes' ); + $this->plugin->settings->ensure_option_autoload(); + + $first = $this->get_option_autoload( $option_key ); + $this->assertContains( $first, self::AUTOLOAD_YES_FAMILY ); + + $this->plugin->settings->ensure_option_autoload(); + + $second = $this->get_option_autoload( $option_key ); + $this->assertSame( $first, $second ); + $this->assertContains( $second, self::AUTOLOAD_YES_FAMILY ); + } + + public function test_ensure_option_autoload_skips_network_option_key() { + if ( ! function_exists( 'wp_set_option_autoload' ) ) { + $this->markTestSkipped( 'wp_set_option_autoload() requires WordPress 6.4+.' ); + } + + if ( $this->is_network_settings_store() ) { + $this->markTestSkipped( 'Network-activated installs write sitemeta, not wp_options.autoload.' ); + } + + $settings = $this->plugin->settings; + $original_key = $settings->option_key; + $network_key = $settings->network_options_key; + + // Seed a blog option under the network key name with autoload=no. + // ensure_option_autoload() must leave it alone when option_key points + // at the network settings key (sitemeta has no autoload flag). + delete_option( $network_key ); + add_option( $network_key, array( 'general_records_ttl' => 30 ), '', 'no' ); + + $before = $this->get_option_autoload( $network_key ); + $settings->option_key = $network_key; + $settings->ensure_option_autoload(); + $after = $this->get_option_autoload( $network_key ); + $settings->option_key = $original_key; + + $this->assertSame( $before, $after ); + $this->assertContains( $after, self::AUTOLOAD_NO_FAMILY ); + + delete_option( $network_key ); + } +}