feat: DesignSystemWriter – Presets über Divis Sync-Ablauf schreiben (Mirror preset-sync)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Jan-Niclas Loosen
2026-10-16 17:34:49 +02:00
co-authored by Claude Sonnet 5.5
parent 6ee8f9258a
commit 3fe83e39d3
6 changed files with 343 additions and 2 deletions
@@ -0,0 +1,139 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Design;
use ET\Builder\Framework\UserRole\UserRole;
use ET\Builder\Packages\Conversion\Conversion;
use ET\Builder\Packages\GlobalData\GlobalPreset;
use LIT\MCP\Integrations\Divi\Release\Call;
use LIT\MCP\Integrations\Divi\Release\DiviAdapter;
use LIT\MCP\Integrations\Divi\Release\DiviLocked;
use LIT\MCP\Integrations\Divi\Release\Manifest;
use LIT\MCP\Integrations\Divi\Release\Mirror;
use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
use RuntimeException;
use WP_Error;
defined('ABSPATH') || exit;
/**
* Adapter "Designsystem" (spec §3) for writing (Etappe 4): presets are saved the way Divi's own sync route does it
* (mirror preset-sync of GlobalPresetController::sync, formats "Nachtrag 5.15.1"): prepare_data() sanitizes the REST
* format, validate_preset_count() refuses a smaller inventory unless presets may be removed, removed presets leave
* Divi's Divi-4 legacy option, save_data() writes the option and clears the static CSS. Chunks and the route's nonce
* are not needed in-process; the tools check the rights themselves (spec §10). Also Divi's preset attribute mapping
* (which attributes a preset may carry) and the Visual Builder role right. Not final so abilities can be tested with
* a mock.
*/
class DesignSystemWriter implements DiviAdapter
{
public const ACTION_SAVE = 'ADD_NEW_MODULE_PRESET';
public const ACTION_DELETE_MODULE = 'DELETE_MODULE_PRESET';
public const ACTION_DELETE_GROUP = 'DELETE_OPTION_GROUP_PRESET';
/** Preset attribute types Divi copies into a preset (Conversion::get_preset_attrs). */
public const PRESET_TYPES = ['style', 'html', 'script'];
public static function manifest(): Manifest
{
return new Manifest(
Manifest::DESIGN,
calls: [
new Call(GlobalPreset::class . '::get_data', 0),
new Call(GlobalPreset::class . '::prepare_data', 1),
new Call(GlobalPreset::class . '::validate_preset_count', 3),
new Call(GlobalPreset::class . '::get_deleted_preset_ids', 3),
new Call(GlobalPreset::class . '::remove_preset_from_legacy_option', 2),
new Call(GlobalPreset::class . '::save_data', 1),
new Call(Conversion::class . '::get_preset_attrs_mapping', 1),
new Call(Conversion::class . '::get_preset_attrs', 3),
new Call(UserRole::class . '::can_current_user_use_visual_builder', 0),
],
mirrors: [
new Mirror(
'preset-sync',
'includes/builder-5/server/Packages/GlobalData/GlobalPresetController.php',
'sync',
'GlobalPresetController',
'Speicherablauf der Presets (prepare_data, validate_preset_count, Bereinigung der Divi-4-Option '
. 'beim Löschen, save_data)'
),
]
);
}
/** @return array<array-key, mixed> The stored inventory (GlobalPreset::get_data(), formats §3). */
public function presets(): array
{
$data = GlobalPreset::get_data();
return is_array($data) ? $data : [];
}
/**
* @param array<array-key, mixed> $rest REST/sync format, see PresetBook::toRest().
* @param bool $mayRemove Deleting or restoring: fewer presets are allowed and removed ones
* leave Divi's legacy option (like a DELETE_* sync).
* @return array<array-key, mixed> The inventory Divi stored.
* @throws DiviLocked
* @throws RuntimeException When Divi refuses the inventory.
*/
public function savePresets(array $rest, bool $mayRemove): array
{
ReleaseGate::assertWritable();
$prepared = GlobalPreset::prepare_data($rest);
$current = GlobalPreset::get_data();
$refusal = GlobalPreset::validate_preset_count(
$current,
$prepared,
$mayRemove ? self::ACTION_DELETE_MODULE : self::ACTION_SAVE
);
if ($refusal instanceof WP_Error) {
throw new RuntimeException('preset sync refused: ' . $refusal->get_error_code() . ' '
. $refusal->get_error_message());
}
if ($mayRemove) {
foreach (['module', 'group'] as $kind) {
foreach (GlobalPreset::get_deleted_preset_ids($current, $prepared, $kind) as $deleted) {
GlobalPreset::remove_preset_from_legacy_option(
(string) ($deleted['id'] ?? ''),
(string) ($deleted['moduleName'] ?? '')
);
}
}
}
$saved = GlobalPreset::save_data($prepared);
return is_array($saved) ? $saved : [];
}
/**
* The parts of $attrs a preset of $moduleName carries (Divi's preset attribute mapping): all, style
* (styleAttrs) and render (renderAttrs: html + script).
*
* @param array<string, mixed> $attrs
* @return array{all: array<string, mixed>, style: array<string, mixed>, render: array<string, mixed>}
*/
public function presetAttrs(string $moduleName, array $attrs): array
{
$map = Conversion::get_preset_attrs_mapping($moduleName);
return [
'all' => (array) Conversion::get_preset_attrs($attrs, self::PRESET_TYPES, $map),
'style' => (array) Conversion::get_preset_attrs($attrs, ['style'], $map),
'render' => (array) Conversion::get_preset_attrs($attrs, ['html', 'script'], $map),
];
}
/** Divi's role right "Visual Builder verwenden"; fail closed without Divi. */
public function canUseVisualBuilder(): bool
{
return class_exists(UserRole::class) && UserRole::can_current_user_use_visual_builder();
}
}
@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Release;
use LIT\MCP\Integrations\Divi\Design\DesignSystemReader;
use LIT\MCP\Integrations\Divi\Design\DesignSystemWriter;
use LIT\MCP\Integrations\Divi\Library\DiviLibraryStore;
use LIT\MCP\Integrations\Divi\Library\DiviRoles;
use LIT\MCP\Integrations\Divi\Pages\DiviPages;
@@ -25,6 +26,7 @@ final class DiviAdapters
DiviRoles::class,
DiviThemeBuilder::class,
DesignSystemReader::class,
DesignSystemWriter::class,
DiviModules::class,
DynamicContentMap::class,
];
@@ -1,5 +1,6 @@
{
"library-submit-layout": "594b3843d4c42e1a91e5eda06f108c79583a0036e46c92dd749f4f541e362d02",
"preset-sync": "9586f8005b8065893c0350977a58ee9171bf715fff21ac0c84663cc512f45522",
"tb-api-save": "7ba452f4d73c9b676281ca6ddcfddd669502965db868bf478ef7804937e5bfc4",
"tb-load-setting-options": "2ea0a6925a1da5522489a417104b384b2bd77a16b25f4122b9503873a4c8a24e",
"tb-store-template": "3bf4ac6028ed99ed8ac3523c659392a5fce7818ef8a80ea226e01f5404eb441d",