Repository navigation
Conversation
`get_option()` and `get_site_option()` return the default both when an option does not exist and when their query fails, and cache the option as missing either way. `WP_Secrets_Option_Store::get()` read that as an absent secret, so during a failed read `wp_get_secret()` returned `null` for a stored secret and `wp_set_secret()` treated it as new, writing a record with no previous version. When the Options API returns the default, the store now checks the database directly. If the row exists, it returns the record and clears the cached "missing" entry. If that read fails too, it returns a `secret_store_unavailable` error, which the write path already treats as fatal. `WP_Secrets_Option_Store::delete()` now uses the same check, so it no longer reports success when it cannot tell whether the record still exists. See #66424.
Adds tests that read a network secret back after the cache is flushed, which catches a second `wp_sitemeta` row left by writing through a stale "missing" cache entry. A row with an empty or `NULL` value now reports `secret_record_malformed` rather than reading as an absent secret. See #66424.
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. Core Committers: Use this line as a base for the props when committing in SVN: To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. Props Bot commandsYou can trigger Props Bot actions by commenting on this PR:
|
Test using WordPress PlaygroundThe changes in this pull request can previewed and tested using a WordPress Playground instance. WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser. Some things to be aware of
For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation. |
ericmann
left a comment
There was a problem hiding this comment.
Thanks @MicahelE. I confirmed the multisite path on trunk: with the record in notoptions, add_network_option() skips get_network_option() and calls $wpdb->insert(), so 15301 as it stands leaves two rows. Clearing the entry in read_stored_value() is the right fix.
delete() going through get() also looks right. A malformed record is a WP_Error, not null, so a failed delete of a row that exists is still reported.
One request, from the ticket: please also make list_names() take failure from $wpdb->query(). Today get_col() always returns an array, so its error check can't fire, and a failed listing reads as an empty one.
|
@MicahelE, r65272 changed the Secrets API tests, so this now likely conflicts. When you merge or rebase trunk, could you also:
|
get_option()andget_site_option()return the default both when an option does not exist and when their query fails, and they cache the option as missing either way.WP_Secrets_Option_Store::get()read that as an absent secret, so during a failed read:wp_get_secret()returnednullfor a stored secret.wp_set_secret()treated the secret as new, returnedtrue, and wrote a record with no previous version.When the Options API returns the default,
get()now checks the database directly:null.secret_store_unavailable, which the write path already treats as fatal.Failure is taken from the return value of
wpdb::query(), becausewpdb::$last_erroris not set on every failure path.delete()now uses the same check, so it no longer reports success when it cannot tell whether the record still exists.Relationship to #15301
#15301 was opened on this ticket first and has the same core approach. This PR differs in two ways:
It clears the cached "missing" entry. Secrets: Tell a failed record read apart from a missing secret. #15301 leaves that entry in place on purpose. On multisite, the next
update_site_option()then goes throughadd_network_option(), which inserts a secondwp_sitemetarow, and later requests read the old one. I ran the same scenario against both: storeone, thentwo, fail one read, then writethreeto a network secret, then flush the cache and read it back.wp_set_network_secret()truethreetwotruethreethreeIt also fixes
delete(), which the Trac comment on Secrets: Tell a failed record read apart from a missing secret. #15301 lists as left out of that patch.The handling of a row with an empty or
NULLvalue comes from that same comment and PR, and I had missed that case. If the committer prefers #15301, the cache cleanup and the test that flushes the cache could move there instead.Known limits
list_names()is not changed.wpdb::get_col()always returns an array, so its error check can't fire and a failed query looks like an empty list.Testing
The new tests make the record's
SELECTfail through aqueryfilter and run in both scopes, plus one multisite-only test. To run them:--group secretshas no failures: 399 tests on single site and 400 on multisite, on PHP 8.2 and MySQL 8.4.Trac ticket: https://core.trac.wordpress.org/ticket/66424
Use of AI Tools
AI assistance: Yes
Tool(s): Claude Code
Model(s): Claude Opus 5.5
Used for: Reproducing the bug, writing the patch and tests, running the checks and the comparison above, and drafting this description.
This Pull Request is for code review only. Please keep all other discussion in the Trac ticket. Do not merge this Pull Request. See GitHub Pull Requests for Code Review in the Core Handbook for more details.