fix(audit L5): unter der Sperre Divis Laufzeit-Caches verwerfen (Options-Cache-Gruppe, GlobalData-/GlobalPreset-Static, et_theme_options) vor raw() und design_rev

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Jan-Niclas Loosen
2026-10-20 00:26:40 +02:00
co-authored by Claude Sonnet 5.5
parent 6e84138979
commit 99c7aab238
28 changed files with 182 additions and 13 deletions
@@ -36,6 +36,11 @@ final class ColorArea implements DesignArea
return DiviFeature::ColorWrite;
}
public function refresh(): void
{
$this->colors->refresh();
}
public function raw(): array
{
return ColorBook::fromDivi($this->colors->read())->data();
@@ -25,6 +25,12 @@ interface DesignArea
/** The Divi feature a write of this area needs (DesignChange asks ReleaseGate::assertFeature() for it). */
public function feature(): DiviFeature;
/**
* Drops what this request cached of the inventory (Divi's static and option caches), called right after the
* lock is taken: another writer may have finished since the request began (audit L5).
*/
public function refresh(): void;
/** @return array<array-key, mixed> */
public function raw(): array;
@@ -93,6 +93,9 @@ final class DesignChange
);
}
try {
// Audit L5: drop Divi's request caches, so the inventory and design_rev below are the stored state.
$area->refresh();
return $this->attempt($ability, $area, $rev, $plan);
} finally {
$this->lock->release($area->name());
+6 -5
View File
@@ -15,8 +15,8 @@ defined('ABSPATH') || exit;
* DiviCheckRoute (add_option() reads first and writes after, so two requests could both win; audit L6). A lock
* older than STALE_AFTER seconds (a request that died) is taken over with a conditional UPDATE on the value just
* read, so only one taker wins; release() deletes the row only while it still holds this instance's token, so a
* holder that lost a stale lock cannot free the new holder's. A taken lock drops the cache of the autoloaded
* options, so the inventory read under the lock (and design_rev) is the stored one.
* holder that lost a stale lock cannot free the new holder's. A taken lock drops the options cache, so the
* inventory read under the lock (and design_rev) is the stored one.
*/
final class DesignLock
{
@@ -66,9 +66,10 @@ final class DesignLock
}
if ($taken) {
$this->tokens[$option] = $token;
// Audit L5: what the holder reads and rewrites must be the stored state, not the request-start cache
// (autoloaded options like Divi's live in "alloptions"); another writer may have finished since.
wp_cache_delete('alloptions', 'options');
// Audit L5: what the holder reads and rewrites must be the stored state, not the request's option cache
// ("alloptions" for autoloaded options, one entry each for the others); another writer may have
// finished since. A cache without group flushing loses everything – rare, and only under the lock.
wp_cache_supports('flush_group') ? wp_cache_flush_group('options') : wp_cache_flush();
}
return $taken;
@@ -81,6 +81,19 @@ class DesignSystemWriter implements DiviAdapter
);
}
/**
* Audit L5: GlobalPreset::get_data() keeps a static copy that can be older than another writer's finished save
* (the option cache is dropped by DesignLock). Called under the lock, before reading.
*/
public function refresh(): void
{
try {
(new \ReflectionProperty(GlobalPreset::class, '_data'))->setValue(null, null);
} catch (\ReflectionException) {
// Divi without that property: its reads are not cached this way.
}
}
/** @return array<array-key, mixed> The stored inventory (GlobalPreset::get_data(), formats §3). */
public function presets(): array
{
@@ -84,7 +84,32 @@ class GlobalColorsAdapter implements DiviAdapter
}
// Audit L5: set_global_colors() ends in et_update_option(), which writes Divi's request global back whole.
$GLOBALS['et_theme_options'] = get_option('et_divi');
$this->refresh();
GlobalData::set_global_colors($clean, ! $replace);
}
/**
* Audit L5: colors are read through et_get_option(), i.e. Divi's request global and GlobalData's static cache –
* both can be older than another writer's finished save. Called under the lock, before reading.
*/
public function refresh(): void
{
$GLOBALS['et_theme_options'] = get_option(self::OPTIONS);
self::forgetCached('colors');
}
/** Resets one entry of GlobalData's static runtime cache (private, so by reflection; gone = nothing to reset). */
private static function forgetCached(string $key): void
{
try {
$cache = new \ReflectionProperty(GlobalData::class, '_cached_data');
$data = $cache->getValue();
if (is_array($data) && array_key_exists($key, $data)) {
$data[$key] = null;
$cache->setValue(null, $data);
}
} catch (\ReflectionException) {
// Divi without that property: its reads are not cached this way.
}
}
}
@@ -86,4 +86,28 @@ class GlobalVariablesAdapter implements DiviAdapter
{
return function_exists('et_pb_is_allowed') && (bool) et_pb_is_allowed('variables_manager');
}
/**
* Audit L5: GlobalData keeps a static copy of the variables that can be older than another writer's finished
* save (the option cache is dropped by DesignLock). Called under the lock, before reading.
*/
public function refresh(): void
{
self::forgetCached('variables');
}
/** Resets one entry of GlobalData's static runtime cache (private, so by reflection; gone = nothing to reset). */
private static function forgetCached(string $key): void
{
try {
$cache = new \ReflectionProperty(GlobalData::class, '_cached_data');
$data = $cache->getValue();
if (is_array($data) && array_key_exists($key, $data)) {
$data[$key] = null;
$cache->setValue(null, $data);
}
} catch (\ReflectionException) {
// Divi without that property: its reads are not cached this way.
}
}
}
@@ -34,6 +34,11 @@ final class PresetArea implements DesignArea
return DiviFeature::PresetWrite;
}
public function refresh(): void
{
$this->writer->refresh();
}
public function raw(): array
{
return PresetBook::fromDivi($this->writer->presets())->data();
@@ -40,6 +40,11 @@ final class ThemeOptionsArea implements DesignArea
return DiviFeature::ThemeOptionWrite;
}
public function refresh(): void
{
// read() asks get_option(), whose cache the lock drops; write() reloads Divi's request global itself.
}
public function raw(): array
{
return ThemeOptionBook::fromDivi($this->options->read())->data();
@@ -40,6 +40,11 @@ final class VariableArea implements DesignArea
return DiviFeature::VariableWrite;
}
public function refresh(): void
{
$this->variables->refresh();
}
public function raw(): array
{
return VariableBook::fromDivi($this->variables->read())->data();