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>
196 lines
8.1 KiB
PHP
196 lines
8.1 KiB
PHP
<?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();
|
||
}
|
||
}
|