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
@@ -43,6 +43,7 @@ final class ColorAreaTest extends TestCase
public static function adapter(array &$stored, array &$saves): GlobalColorsAdapter
{
$colors = Mockery::mock(GlobalColorsAdapter::class);
$colors->allows('refresh');
$colors->allows('read')->andReturnUsing(static function () use (&$stored): array {
return $stored;
});
@@ -47,6 +47,7 @@ final class ColorRestorePlannerTest extends TestCase
private static function planner(bool $visualBuilder = true, ?GlobalUsages $usages = null): ColorRestorePlanner
{
$writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('canUseVisualBuilder')->andReturn($visualBuilder);
return new ColorRestorePlanner(
@@ -56,6 +56,9 @@ final class DesignChangeTest extends TestCase
public array $restores = [];
/** @var list<string> refresh()/raw() in the order they happened */
public array $calls = [];
public function __construct(
private array &$stored,
private int $failWrites,
@@ -79,8 +82,15 @@ final class DesignChangeTest extends TestCase
return DiviFeature::PresetWrite;
}
public function refresh(): void
{
$this->calls[] = 'refresh';
}
public function raw(): array
{
$this->calls[] = 'raw';
return $this->stored;
}
@@ -205,6 +215,25 @@ final class DesignChangeTest extends TestCase
self::assertSame([], $area->writes);
}
/** Audit L5 (re-review): under the lock Divi's request caches are dropped before the inventory is read. */
public function testTheAreaDropsItsCachesUnderTheLockBeforeReading(): void
{
$stored = ['v' => 1];
$area = self::area($stored);
$snapshots = Mockery::mock(SnapshotStore::class);
$snapshots->allows('save')->andReturn('presets-1');
$snapshots->allows('attachLog');
(new DesignChange($snapshots, new FakeChangeLog(1), new DesignLock()))->run(
'lit/divi-presets-set',
$area,
['confirm' => true, 'design_rev' => $area->rev($stored)],
self::plan(['v' => 2])
);
self::assertSame(['refresh', 'raw'], array_slice($area->calls, 0, 2));
}
/** Final review I-5: a confirm while another change of the area runs writes nothing. */
public function testAHeldLockRefusesTheConfirm(): void
{
@@ -102,26 +102,33 @@ final class DesignLockTest extends TestCase
public function testTheLockHolderReadsEtDiviFreshNotFromTheRequestStartCache(): void
{
$db = $this->table();
$stored = ['et_divi' => ['color' => 'alt']];
$stored = ['et_divi' => ['color' => 'alt'], 'et_divi_global_variables' => ['v' => 'alt']];
$cache = [];
// Like WordPress: every option is cached under its own key (not autoloaded ones) or in "alloptions".
Functions\when('get_option')->alias(static function (string $name) use (&$stored, &$cache) {
if (! isset($cache['options:' . $name]) && ! isset($cache['options:alloptions'][$name])) {
$cache['options:alloptions'][$name] = $stored[$name] ?? false;
}
$key = 'options:' . $name;
$cache[$key] ??= $stored[$name] ?? false;
return $cache['options:' . $name] ?? $cache['options:alloptions'][$name];
return $cache[$key];
});
Functions\when('wp_cache_delete')->alias(static function (string $key, string $group = '') use (&$cache): bool {
unset($cache[$group . ':' . $key]);
Functions\when('wp_cache_flush_group')->alias(static function (string $group) use (&$cache): bool {
foreach (array_keys($cache) as $key) {
if (str_starts_with($key, $group . ':')) {
unset($cache[$key]);
}
}
return true;
});
self::assertSame(['color' => 'alt'], get_option('et_divi'), 'request B cached the old array');
self::assertSame(['v' => 'alt'], get_option('et_divi_global_variables'));
$stored['et_divi'] = ['color' => 'neu']; // request A wrote and released meanwhile
$stored['et_divi_global_variables'] = ['v' => 'neu'];
self::assertTrue((new DesignLock())->acquire('colors'));
self::assertSame(['color' => 'neu'], get_option('et_divi'));
self::assertSame(['v' => 'neu'], get_option('et_divi_global_variables'), 'also options that are not autoloaded');
self::assertNotSame([], $db->rows);
}
}
@@ -18,6 +18,7 @@ final class DesignRightsTest extends TestCase
public function testPresetsNeedEditThemeOptionsAndTheVisualBuilder(): void
{
$writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('canUseVisualBuilder')->andReturn(true, false);
Functions\when('current_user_can')->alias(static fn (string $cap): bool => $cap === 'edit_theme_options');
@@ -33,6 +34,7 @@ final class DesignRightsTest extends TestCase
public function testColorsNeedEditThemeOptionsAndTheVisualBuilder(): void
{
$writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('canUseVisualBuilder')->andReturn(true, false);
Functions\when('current_user_can')->alias(static fn (string $cap): bool => $cap === 'edit_theme_options');
@@ -49,8 +51,10 @@ final class DesignRightsTest extends TestCase
public function testVariablesAlsoNeedTheVariablesManager(): void
{
$writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('canUseVisualBuilder')->andReturn(true);
$variables = Mockery::mock(GlobalVariablesAdapter::class);
$variables->allows('refresh');
$variables->allows('canManage')->andReturn(true, false);
Functions\when('current_user_can')->alias(static fn (string $cap): bool => $cap === 'edit_theme_options');
@@ -63,8 +67,10 @@ final class DesignRightsTest extends TestCase
public function testVariablesAskTheThemeRightAndTheVisualBuilderFirst(): void
{
$writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('canUseVisualBuilder')->andReturn(false);
$variables = Mockery::mock(GlobalVariablesAdapter::class);
$variables->allows('refresh');
$variables->allows('canManage')->andReturn(true);
Functions\when('current_user_can')->justReturn(true);
@@ -53,6 +53,7 @@ final class DiviColorsSetAbilityTest extends TestCase
bool $visualBuilder = true
): DiviColorsSetAbility {
$writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('canUseVisualBuilder')->andReturn($visualBuilder);
$writer->allows('presets')->andReturn($presets === [] ? PresetBookTest::stored() : $presets);
$counter = 0;
@@ -297,6 +298,7 @@ final class DiviColorsSetAbilityTest extends TestCase
{
$calls = [];
$colors = Mockery::mock(GlobalColorsAdapter::class);
$colors->allows('refresh');
$colors->allows('read')->andReturn(ColorAreaTest::stored());
$colors->allows('save')->andReturnUsing(static function (array $entries, bool $replace) use (&$calls): void {
$calls[] = $replace;
@@ -80,6 +80,7 @@ final class DiviDesignDetachAbilityTest extends TestCase
private static function readOnlyWriter(?array $stored = null): DesignSystemWriter&MockInterface
{
$writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('presets')->andReturn($stored ?? PresetBookTest::stored());
$writer->allows('canUseVisualBuilder')->andReturn(true);
$writer->expects('savePresets')->never();
@@ -91,6 +92,7 @@ final class DiviDesignDetachAbilityTest extends TestCase
private static function variableAdapter(bool $manage = true, string $radius = '8px'): GlobalVariablesAdapter&MockInterface
{
$variables = Mockery::mock(GlobalVariablesAdapter::class);
$variables->allows('refresh');
$variables->allows('read')->andReturn(self::variables($radius));
$variables->allows('canManage')->andReturn($manage);
$variables->expects('save')->never();
@@ -246,6 +248,7 @@ final class DiviDesignDetachAbilityTest extends TestCase
$calls = [];
$stored = self::storedWithVariable();
$writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('presets')->andReturn($stored);
$writer->allows('canUseVisualBuilder')->andReturn(true);
$writer->expects('savePresets')
@@ -451,6 +454,7 @@ final class DiviDesignDetachAbilityTest extends TestCase
{
$calls = [];
$writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('presets')->andReturn(PresetBookTest::stored());
$writer->allows('canUseVisualBuilder')->andReturn(true);
$writer->expects('savePresets')
@@ -502,6 +506,7 @@ final class DiviDesignDetachAbilityTest extends TestCase
public function testAPageErrorAfterThePresetStepSaysThePresetsAreChanged(): void
{
$writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('presets')->andReturn(PresetBookTest::stored());
$writer->allows('canUseVisualBuilder')->andReturn(true);
$writer->expects('savePresets')
@@ -633,6 +638,7 @@ final class DiviDesignDetachAbilityTest extends TestCase
private function abilityAfterThePresetStep(PageRewriter $pages): DiviDesignDetachAbility
{
$writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('presets')->andReturn(PresetBookTest::stored());
$writer->allows('canUseVisualBuilder')->andReturn(true);
$writer->expects('savePresets')
@@ -42,6 +42,7 @@ final class DiviDesignRestoreAbilityTest extends TestCase
$older = PresetBookTest::stored();
unset($older['module']['divi/button']['items']['bbbbbbbbbb']);
$writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('presets')->andReturn(PresetBookTest::stored());
$writer->allows('canUseVisualBuilder')->andReturn(true);
$writer->expects('savePresets')->with(Mockery::type('array'), true)->andReturn($older);
@@ -73,6 +74,7 @@ final class DiviDesignRestoreAbilityTest extends TestCase
{
Functions\when('current_user_can')->justReturn(true);
$writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('presets')->andReturn($current);
$writer->allows('canUseVisualBuilder')->andReturn(true);
$snapshots = Mockery::mock(SnapshotStore::class);
@@ -108,6 +110,7 @@ final class DiviDesignRestoreAbilityTest extends TestCase
{
Functions\when('current_user_can')->justReturn(false);
$writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('canUseVisualBuilder')->andReturn(true);
$writer->shouldNotReceive('savePresets');
$snapshots = Mockery::mock(SnapshotStore::class);
@@ -144,6 +147,7 @@ final class DiviDesignRestoreAbilityTest extends TestCase
{
Functions\when('current_user_can')->justReturn(true);
$writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('canUseVisualBuilder')->andReturn(true);
$snapshots = Mockery::mock(SnapshotStore::class);
$snapshots->shouldNotReceive('get');
@@ -171,6 +175,7 @@ final class DiviDesignRestoreAbilityTest extends TestCase
): DiviDesignRestoreAbility {
Functions\when('current_user_can')->justReturn(true);
$writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('canUseVisualBuilder')->andReturn(true);
$writer->allows('presets')->andReturn(PresetBookTest::stored());
$writer->shouldNotReceive('savePresets');
@@ -206,6 +211,7 @@ final class DiviDesignRestoreAbilityTest extends TestCase
private static function colorStore(array $stored): GlobalColorsAdapter&MockInterface
{
$colors = Mockery::mock(GlobalColorsAdapter::class);
$colors->allows('refresh');
$colors->allows('read')->andReturnUsing(static function () use (&$stored): array {
return $stored;
});
@@ -225,6 +231,7 @@ final class DiviDesignRestoreAbilityTest extends TestCase
private static function variableStore(array $stored, bool $manage = true): GlobalVariablesAdapter&MockInterface
{
$variables = Mockery::mock(GlobalVariablesAdapter::class);
$variables->allows('refresh');
$variables->allows('canManage')->andReturn($manage);
$variables->allows('read')->andReturnUsing(static function () use (&$stored): array {
return $stored;
@@ -19,6 +19,7 @@ final class DiviPresetGetAbilityTest extends TestCase
private function ability(?GlobalUsages $usages = null): DiviPresetGetAbility
{
$writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('presets')->andReturn(PresetBookTest::stored());
$usages ??= Mockery::mock(GlobalUsages::class);
@@ -29,6 +29,7 @@ final class DiviPresetsAssignAbilityTest extends TestCase
private function ability(PageRewriter $pages): DiviPresetsAssignAbility
{
$writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('presets')->andReturn(PresetBookTest::stored());
$writer->allows('canUseVisualBuilder')->andReturn(true);
@@ -37,6 +37,7 @@ final class DiviPresetsDeleteAbilityTest extends TestCase
Functions\when('current_user_can')->justReturn(true);
if ($writer === null) {
$writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('presets')->andReturn($stored === [] ? PresetBookTest::stored() : $stored);
$writer->allows('canUseVisualBuilder')->andReturn(true);
}
@@ -59,6 +60,7 @@ final class DiviPresetsDeleteAbilityTest extends TestCase
public function testUnusedPresetIsDeletedAfterConfirm(): void
{
$writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('presets')->andReturn(PresetBookTest::stored());
$writer->allows('canUseVisualBuilder')->andReturn(true);
$writer->expects('savePresets')
@@ -153,6 +155,7 @@ final class DiviPresetsDeleteAbilityTest extends TestCase
0, 1, [], 0, false);
Functions\when('current_user_can')->justReturn(true);
$writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('presets')->andReturn(PresetBookTest::stored());
$writer->allows('canUseVisualBuilder')->andReturn(true);
$usages = Mockery::mock(GlobalUsages::class);
@@ -52,6 +52,7 @@ final class DiviPresetsSetAbilityTest extends TestCase
private function savingWriter(): DesignSystemWriter
{
$writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('presets')->andReturnUsing(fn (): array => $this->stored);
$writer->allows('canUseVisualBuilder')->andReturn(true);
$writer->allows('savePresets')->andReturnUsing(function (array $rest, bool $mayRemove): array {
@@ -291,6 +292,7 @@ final class DiviPresetsSetAbilityTest extends TestCase
public function testNothingIsWrittenWhenOneItemHasErrors(): void
{
$writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('presets')->andReturnUsing(fn (): array => $this->stored);
$writer->allows('canUseVisualBuilder')->andReturn(true);
$writer->shouldNotReceive('savePresets');
@@ -53,6 +53,7 @@ final class DiviVariablesSetAbilityTest extends TestCase
?FontCatalog $fonts = null
): DiviVariablesSetAbility {
$writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('canUseVisualBuilder')->andReturn(true);
$writer->allows('presets')->andReturn($presets === [] ? PresetBookTest::stored() : $presets);
$counter = 0;
@@ -313,6 +314,7 @@ final class DiviVariablesSetAbilityTest extends TestCase
{
$calls = [];
$variables = Mockery::mock(GlobalVariablesAdapter::class);
$variables->allows('refresh');
$variables->allows('canManage')->andReturn(true);
$variables->allows('read')->andReturn(VariableAreaTest::stored());
$variables->allows('save')->andReturnUsing(static function (array $data, array $expected) use (&$calls): void {
@@ -16,6 +16,7 @@ final class PresetAreaTest extends TestCase
public function testWritesTheRestFormatAndReadsBack(): void
{
$writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$stored = PresetBookTest::stored();
$writer->allows('presets')->andReturn($stored);
$writer->expects('savePresets')
@@ -50,6 +51,7 @@ final class PresetAreaTest extends TestCase
public function testRestoreReplacesTheWholeInventory(): void
{
$writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$stored = PresetBookTest::stored();
$writer->expects('savePresets')->with(PresetBook::fromDivi($stored)->toRest(), true)->andReturn($stored);
@@ -57,6 +57,7 @@ final class VariableAreaTest extends TestCase
public static function adapter(array &$stored, array &$saves, bool $manage = true): GlobalVariablesAdapter
{
$variables = Mockery::mock(GlobalVariablesAdapter::class);
$variables->allows('refresh');
$variables->allows('canManage')->andReturn($manage);
$variables->allows('read')->andReturnUsing(static function () use (&$stored): array {
return $stored;
@@ -47,8 +47,10 @@ final class VariableRestorePlannerTest extends TestCase
private static function planner(bool $manage = true, ?GlobalUsages $usages = null): VariableRestorePlanner
{
$writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('canUseVisualBuilder')->andReturn(true);
$variables = Mockery::mock(GlobalVariablesAdapter::class);
$variables->allows('refresh');
$variables->allows('canManage')->andReturn($manage);
return new VariableRestorePlanner($writer, $variables, $usages ?? UsageFixture::none());
@@ -57,6 +57,7 @@ final class DiviCompositionTest extends TestCase
private function presetSource(int $reads = 1): DesignSystemWriter
{
$writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->expects('presets')->times($reads)->andReturn([
'module' => ['divi/button' => ['default' => 'p-1', 'items' => [
'p-1' => ['id' => 'p-1', 'name' => 'Standard', 'moduleName' => 'divi/button'],
@@ -211,6 +212,7 @@ final class DiviCompositionTest extends TestCase
public function testThePresetBookIsNotReadWithoutAPresetInTheInput(): void
{
$writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->shouldNotReceive('presets');
$writer->shouldNotReceive('groupsOf');