fix: verify option writes before treating update_option() false as failure

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
This commit is contained in:
2026-09-26 00:47:35 +02:00
co-authored by Claude Opus 5.5
parent 9cc9d4f2dc
commit fc216e3eaf
4 changed files with 109 additions and 12 deletions
@@ -101,7 +101,9 @@ final class SettingsWhitelist
private function checkLanguage(mixed $value): ?string
{
if ($value === 'en_US' || $value === '') {
// Clients say en_US for English; '' is WordPress' internal storage form for it
// (see normalize()) and is not accepted as input.
if ($value === 'en_US') {
return null;
}
@@ -138,7 +138,7 @@ final class WpSettingsUpdateAbility implements Ability
try {
foreach ($settings as $key => $value) {
$key = (string) $key;
if ($previous[$key] === $value) {
if ($this->isStored($key, $this->whitelist->normalize($key, $value))) {
continue;
}
@@ -168,17 +168,34 @@ final class WpSettingsUpdateAbility implements Ability
private function writeOne(string $key, mixed $value): void
{
$normalized = $this->whitelist->normalize($key, $value);
if ($key === 'permalink_structure') {
$this->setPermalinkStructure((string) $value);
$this->setPermalinkStructure((string) $normalized);
return;
}
if (update_option($key, $this->whitelist->normalize($key, $value)) === false) {
throw new RuntimeException(sprintf('update_option(%s) returned false', $key));
// update_option() returns false both for a genuine failure and for a value
// that already matches the stored option (WordPress core short-circuits the
// write in that case, see wp-includes/option.php) – get_option() afterwards
// tells the two apart, the same way MetaFieldSource::holds() does for meta.
if (update_option($key, $normalized) === false && ! $this->isStored($key, $normalized)) {
throw new RuntimeException(sprintf('update_option(%s) failed', $key));
}
}
/** Whether option $key already holds $normalized, compared like WordPress compares scalars. */
private function isStored(string $key, mixed $normalized): bool
{
$stored = get_option($key);
if (is_scalar($stored) && is_scalar($normalized)) {
return (string) $stored === (string) $normalized;
}
return $stored == $normalized; // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual
}
/**
* A write failed; restores the keys already written to their previous values and
* reports write_failed (fully restored) or partial_write (some keys could not be
@@ -223,5 +240,11 @@ final class WpSettingsUpdateAbility implements Ability
global $wp_rewrite;
$wp_rewrite->set_permalink_structure($structure);
flush_rewrite_rules();
// set_permalink_structure() has no return value to check; verify the option
// itself so a failure here is caught and rolled back like any other setting.
if (! $this->isStored('permalink_structure', $structure)) {
throw new RuntimeException('set_permalink_structure() did not update the stored value.');
}
}
}