Skip to content

Keep site flag defaults and saved state across requests - #32

Open
svandragt wants to merge 2 commits into
masterfrom
fix-site-flag-persistence
Open

svandragt wants to merge 2 commits into
masterfrom
fix-site-flag-persistence

Conversation

@svandragt

@svandragt svandragt commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Site-scoped flags lose their state in three ways. A flag registered as active by default starts inactive. A flag turned on by code outside wp-admin reads back as off on the next request. Saving Settings → General can switch off a flag that is active by default.

What changed

  • With nothing stored, a site flag now keeps the default it was registered with. Before, the stored-state lookup fell back to true, which never matched 'active', so the flag was switched off.1
  • Saving a site flag now stores 'active' or 'inactive', the same as user flags. Before, it stored a bool, which reads back as off. The admin bar toggle worked only because the settings sanitise callback converted the value on admin-ajax.php. Code that set the flag on the front end or in WP-CLI lost the state.2
  • The Settings → General checkbox now shows the flag's resolved state. Before, it showed unchecked for a default-active flag with nothing stored, so saving the page stored 'inactive'.3

Test plan

  • A smoke script with stubbed WordPress option functions covers each case, with and without the sanitise filter. Before the fix, the default-active and save-on cases are wrong, and save-off writes nothing. After the fix, all of them give the expected state.4
  • On a wp-env install (WordPress 7.1.3, PHP 8.2), master shows all three bugs and this branch fixes them. The test flag is a site flag registered with 'active' => true, and every check starts with nothing stored.5
  • The linter passes (exit 0).
Check master This PR Expected
Nothing stored inactive active active
Turned off from code (WP-CLI) inactive, nothing stored inactive, 'inactive' stored inactive
Turned on from code (WP-CLI) inactive, '1' stored active, 'active' stored active
Admin bar toggle, twice flips correctly flips correctly flips
Settings → General checkbox unchecked checked checked
Settings → General saved unchanged inactive, 'inactive' stored active, 'active' stored active

The admin bar toggle already worked on master, because the settings sanitise callback converts the value on admin-ajax.php.

Smoke output

Before:

Case Default Stored active
Nothing stored true — false ✗
Save on, no filter either '1' false ✗
Save off, no filter either nothing false, but only because the first bug already lost the default
Filter attached either 'active' / 'inactive' true / false

After: every case reads back what was saved, and the default holds when nothing is stored.

This is independent of #26 (Lint PHP and report Composer package changes in CI) and #31 (Fix the WPCS code execution advisory and require PHP 8.2). If #31 merges first, this needs a rebase for return type spacing. The changed lines don't touch return types, so I expect no conflict.

Footnotes

  1. get_option( $key, true, '' ) → get_option( $key, '' ) in Site\handle(), inc/site.php. get_option() takes two arguments, so the third was ignored. ↩

  2. update_option( $key, $value ) → update_option( $key, $value ? 'active' : 'inactive' ) in Site\save(). The sanitise callback is SiteMetabox\sanitize_value(), registered on admin_init. ↩

  3. SiteMetabox\render() in inc/site-metabox.php now uses $flag->active, which Site\handle() sets on init. ↩

  4. The script stubs get_option, update_option and the sanitize_option_{$option} filter, then loads inc/class-flag.php, inc/class-flags.php and inc/site.php. It ran on PHP 8.2. ↩

  5. A must-use plugin registers the flag on init. Each check runs wp option delete _wp_flag_demo first and reads active in a new wp eval process. The admin bar check logs in, takes the toggle URL from the admin bar HTML and requests it. The Settings check submits the General form unchanged to options.php. ↩

svandragt and others added 2 commits October 16, 2026 12:03
Site\handle() read the option with get_option( $key, true, '' ).
get_option() takes two arguments, so the third was ignored and the
default was true. With nothing stored, the code then compared true
with 'active' and set the flag inactive, which overrode the default
of any site flag registered as active.

Site\save() passed a PHP bool to update_option(). WordPress stores
true as '1', which handle() never matches against 'active', and
stores false as '' (or skips the write when no row exists), which
handle() treats as unset. The sanitize callback registered in
site-metabox.php hid this on admin-ajax.php only, because it runs
once admin_init has fired. Code that sets the flag on the front end
or in WP-CLI wrote the bare bool and lost the state.

Read with '' as the default and write 'active' or 'inactive', as
User\save() does, so the round trip no longer depends on the
sanitize filter.

Co-Authored-By: Claude <noreply@anthropic.com>
render() read the option with the same get_option( $key, true, '' )
call as Site\handle(), so a site flag that is active by default and
has nothing stored showed as unchecked. Saving the General settings
page for any reason then stored 'inactive' and switched the flag off.

Site\handle() has already applied any stored value to the flag on
init, so render the flag's own active state.

Co-Authored-By: Claude <noreply@anthropic.com>
@svandragt svandragt self-assigned this Oct 16, 2026
@svandragt
svandragt marked this pull request as draft October 16, 2026 13:01
@svandragt
svandragt marked this pull request as ready for review October 16, 2026 14:46
@svandragt
svandragt requested a review from tomjn October 16, 2026 14:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant