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:
co-authored by
Claude Sonnet 5.5
parent
6e84138979
commit
99c7aab238
@@ -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());
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user