Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 38 additions & 3 deletions classes/class-settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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.
*
Expand Down Expand Up @@ -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,
)
);

Expand Down
177 changes: 177 additions & 0 deletions tests/phpunit/test-class-settings.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
<?php
/**
* Tests for Settings option autoload behaviour (issue #1482).
*
* @package WP_Stream
*/

namespace WP_Stream;

/**
* Class - Test_Settings
*/
class Test_Settings extends WP_StreamTestCase {

/**
* Autoload column values that mean "load on every request".
*
* Modern WordPress may store on / auto-on instead of the legacy yes.
*
* @var string[]
*/
const AUTOLOAD_YES_FAMILY = array( 'yes', 'on', 'auto-on' );

/**
* Autoload column values that mean "do not autoload".
*
* @var string[]
*/
const AUTOLOAD_NO_FAMILY = array( 'no', 'off', 'auto-off' );

/**
* Read the autoload column for a blog option.
*
* @param string $option_name Option name.
* @return string|null Autoload value, or null if the row is missing.
*/
private function get_option_autoload( $option_name ) {
global $wpdb;

$value = $wpdb->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 );
}
}