Files
WP-LIT-MCP/src/Integrations/Divi/Design/DesignSystemWriter.php
T
Jan-Niclas LoosenandClaude Opus 5.5 b511ef6348 refactor(compat): Prüfbausteine nach src/Compatibility, Matrix und ManifestCheck generisch
Requirement, Call, ConstantRequirement, Mirror, Problem, FeatureStatus, CompatibilityMatrix, Manifest, ManifestCheck sowie Fingerprints und LineDiff liegen jetzt im divi-freien Kern. Neu: GlobalRequirement, CompatibilityMatrix::fromProblems(), ManifestCheck-Option signatures, DiviManifestCheck::create() und DiviAreas. Divi-Verhalten unverändert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 12:03:30 +01:00

196 lines
8.1 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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 ET\Builder\Packages\ModuleLibrary\ModuleRegistration;
use LIT\MCP\Compatibility\Call;
use LIT\MCP\Integrations\Divi\Release\DiviAdapter;
use LIT\MCP\Integrations\Divi\Release\DiviAreas;
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
use LIT\MCP\Integrations\Divi\Release\DiviLocked;
use LIT\MCP\Compatibility\Manifest;
use LIT\MCP\Compatibility\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(
DiviAreas::DESIGN,
calls: [
// Read by the preset tools as well as by the design system tool.
new Call(GlobalPreset::class . '::get_data', 0, [DiviFeature::DesignRead, DiviFeature::PresetWrite]),
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),
// groupsOf(): also used by the composer for group presets (insert/update/assign), so Catalog too.
new Call(
GlobalPreset::class . '::get_group_preset_default_attr',
1,
[DiviFeature::Catalog, DiviFeature::PresetWrite]
),
new Call(
ModuleRegistration::class . '::get_module_settings',
1,
[DiviFeature::Catalog, DiviFeature::PresetWrite]
),
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)'
),
],
features: [DiviFeature::PresetWrite]
);
}
/**
* 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
{
$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 When preset-write is not released – before anything is written.
* @throws RuntimeException When Divi refuses the inventory.
*/
public function savePresets(array $rest, bool $mayRemove): array
{
ReleaseGate::assertFeature(DiviFeature::PresetWrite);
$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 option groups of $moduleName that take group presets: attribute path (groupId) => group name, e.g.
* "module.decoration.spacing" => "divi/spacing" (formats §3). [] without Divi or for an unknown module.
*
* @return array<string, string>
*/
public function groupsOf(string $moduleName): array
{
if (! class_exists(ModuleRegistration::class)) {
return [];
}
$settings = ModuleRegistration::get_module_settings($moduleName);
if ($settings === null) {
return [];
}
$groups = [];
foreach (GlobalPreset::get_group_preset_default_attr($settings) as $groupId => $group) {
if (is_array($group) && is_string($group['groupName'] ?? null)) {
$groups[(string) $groupId] = $group['groupName'];
}
}
return $groups;
}
/**
* 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();
}
}