feat: Fähigkeiten color-write/variable-write; GlobalColorsAdapter und GlobalVariablesAdapter (Manifest, Signatur-Referenzen) und DesignRights::colors()/variables()
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
d73144ab36
commit
9b7c3c1d3b
@@ -11,9 +11,10 @@ use WP_Error;
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* Who may change the design system (spec §10): presets need edit_theme_options and Divi's role right "Visual Builder
|
||||
* verwenden" – what Divi's own preset sync asks since 5.15 (formats "Nachtrag 5.15.1"). Without unfiltered_html a
|
||||
* write is refused while presets it saves along hold HTML/CSS that Divi would strip (final review I-2).
|
||||
* Who may change the design system (spec §10): presets and colors need edit_theme_options and Divi's role right
|
||||
* "Visual Builder verwenden" – what Divi's own preset sync asks since 5.15 (formats "Nachtrag 5.15.1"); variables
|
||||
* also the role right "Variables Manager". Without unfiltered_html a write is refused while presets it saves along
|
||||
* hold HTML/CSS that Divi would strip (final review I-2).
|
||||
*/
|
||||
final class DesignRights
|
||||
{
|
||||
@@ -48,15 +49,46 @@ final class DesignRights
|
||||
}
|
||||
|
||||
public static function presets(DesignSystemWriter $writer): ?WP_Error
|
||||
{
|
||||
return self::designer($writer, 'Presets');
|
||||
}
|
||||
|
||||
/** Colors: the same rights as presets (spec §10 – stricter than Divi, which asks edit_theme_options only). */
|
||||
public static function colors(DesignSystemWriter $writer): ?WP_Error
|
||||
{
|
||||
return self::designer($writer, 'Farben');
|
||||
}
|
||||
|
||||
/** Variables: also Divi's role right "Variables Manager" – without it Divi silently saves nothing (plan 4c V7). */
|
||||
public static function variables(DesignSystemWriter $writer, GlobalVariablesAdapter $variables): ?WP_Error
|
||||
{
|
||||
$refusal = self::designer($writer, 'Variablen');
|
||||
if ($refusal !== null || $variables->canManage()) {
|
||||
return $refusal;
|
||||
}
|
||||
|
||||
return Errors::forbidden(
|
||||
'Die Divi-Rolle dieses Kontos erlaubt den Variablen-Manager nicht; ohne ihn speichert Divi keine '
|
||||
. 'Variablen.',
|
||||
'Einen Administrator bitten, das Recht unter Divi → Rollen-Editor freizugeben.'
|
||||
);
|
||||
}
|
||||
|
||||
/** edit_theme_options and Divi's role right "Visual Builder verwenden"; $what names the area in the message. */
|
||||
private static function designer(DesignSystemWriter $writer, string $what): ?WP_Error
|
||||
{
|
||||
if (! current_user_can('edit_theme_options')) {
|
||||
return Errors::forbidden(
|
||||
'Presets ändern erfordert das Recht, das Design der Website anzupassen (edit_theme_options).'
|
||||
);
|
||||
return Errors::forbidden(sprintf(
|
||||
'%s ändern erfordert das Recht, das Design der Website anzupassen (edit_theme_options).',
|
||||
$what
|
||||
));
|
||||
}
|
||||
if (! $writer->canUseVisualBuilder()) {
|
||||
return Errors::forbidden(
|
||||
'Die Divi-Rolle dieses Kontos erlaubt den Visual Builder nicht; ohne ihn ändert LIT MCP keine Presets.',
|
||||
sprintf(
|
||||
'Die Divi-Rolle dieses Kontos erlaubt den Visual Builder nicht; ohne ihn ändert LIT MCP keine %s.',
|
||||
$what
|
||||
),
|
||||
'Einen Administrator bitten, das Recht unter Divi → Rollen-Editor freizugeben.'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Design;
|
||||
|
||||
use ET\Builder\Packages\GlobalData\GlobalData;
|
||||
use LIT\MCP\Integrations\Divi\Release\Call;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviAdapter;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviLocked;
|
||||
use LIT\MCP\Integrations\Divi\Release\Manifest;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
|
||||
use RuntimeException;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* Adapter "Designsystem" (spec §3) for the global colors (formats §1, plan 4c V3/V4): reads them through
|
||||
* GlobalData::get_global_colors() and writes them through set_global_colors(). The five customizer colors live in
|
||||
* Divi's theme options; get_customizer_colors() caches them in a static variable for the whole request, so read()
|
||||
* lays the stored option values over Divi's answer – a read-back after writing sees the new value. Not final so the
|
||||
* color area can be tested with a mock.
|
||||
*/
|
||||
class GlobalColorsAdapter implements DiviAdapter
|
||||
{
|
||||
/** Divi's theme options (one array option) with the customizer colors. */
|
||||
private const OPTIONS = 'et_divi';
|
||||
|
||||
/** Customizer color ID => its option in et_divi (GlobalData::$customizer_colors). */
|
||||
private const CUSTOMIZER = [
|
||||
'gcid-primary-color' => 'accent_color',
|
||||
'gcid-secondary-color' => 'secondary_accent_color',
|
||||
'gcid-heading-color' => 'header_color',
|
||||
'gcid-body-color' => 'font_color',
|
||||
'gcid-link-color' => 'link_color',
|
||||
];
|
||||
|
||||
public static function manifest(): Manifest
|
||||
{
|
||||
return new Manifest(
|
||||
Manifest::DESIGN,
|
||||
calls: [
|
||||
new Call(GlobalData::class . '::get_global_colors', 0),
|
||||
new Call(GlobalData::class . '::sanitize_global_colors_data', 1),
|
||||
new Call(GlobalData::class . '::set_global_colors', 2),
|
||||
],
|
||||
keys: [self::OPTIONS],
|
||||
features: [DiviFeature::ColorWrite]
|
||||
);
|
||||
}
|
||||
|
||||
/** @return array<string, array<string, mixed>> get_global_colors() with the customizer values of et_divi. */
|
||||
public function read(): array
|
||||
{
|
||||
$colors = GlobalData::get_global_colors();
|
||||
$options = get_option(self::OPTIONS);
|
||||
foreach (self::CUSTOMIZER as $id => $option) {
|
||||
$value = is_array($options) ? ($options[$option] ?? null) : null;
|
||||
if (is_array($colors[$id] ?? null) && is_string($value) && $value !== '') {
|
||||
$colors[$id]['color'] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $colors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves checked entries: $replace false merges them into the stored colors (set_global_colors with
|
||||
* "already sanitized"), $replace true replaces all stored colors (restore). Customizer colors among them go to
|
||||
* the theme options (Divi does that itself).
|
||||
*
|
||||
* @param array<string, array<string, mixed>> $entries
|
||||
* @throws DiviLocked When color-write is not released – before anything is written.
|
||||
* @throws RuntimeException When Divi's sanitizing drops an entry; nothing is written then.
|
||||
*/
|
||||
public function save(array $entries, bool $replace): void
|
||||
{
|
||||
ReleaseGate::assertFeature(DiviFeature::ColorWrite);
|
||||
$clean = GlobalData::sanitize_global_colors_data($entries);
|
||||
$dropped = array_diff(array_keys($entries), array_keys($clean));
|
||||
if ($dropped !== []) {
|
||||
throw new RuntimeException('global colors refused: Divi dropped ' . implode(', ', $dropped));
|
||||
}
|
||||
|
||||
GlobalData::set_global_colors($clean, ! $replace);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Design;
|
||||
|
||||
use ET\Builder\Packages\GlobalData\GlobalData;
|
||||
use LIT\MCP\Integrations\Divi\Release\Call;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviAdapter;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviLocked;
|
||||
use LIT\MCP\Integrations\Divi\Release\Manifest;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
|
||||
use RuntimeException;
|
||||
use WP_Error;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* Adapter "Designsystem" (spec §3) for the global variables (formats §2, plan 4c V7): reads them through
|
||||
* GlobalData::get_global_variables() back into the stored form and writes them through set_global_variables() with
|
||||
* the inventory the change was planned on (Divi's own stale check). Without edit_theme_options or Divi's role right
|
||||
* "Variables Manager" Divi silently saves nothing – DesignRights::variables() asks canManage() first. Not final so
|
||||
* the variable area can be tested with a mock.
|
||||
*/
|
||||
class GlobalVariablesAdapter implements DiviAdapter
|
||||
{
|
||||
public static function manifest(): Manifest
|
||||
{
|
||||
return new Manifest(
|
||||
Manifest::DESIGN,
|
||||
calls: [
|
||||
new Call(GlobalData::class . '::get_global_variables', 0),
|
||||
new Call(GlobalData::class . '::set_global_variables', 2),
|
||||
new Call('et_pb_is_allowed', 1),
|
||||
],
|
||||
features: [DiviFeature::VariableWrite]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The stored form: type => ID => entry as arrays, without allowedActions, without the customizer fonts and the
|
||||
* group "colors" (both are not stored there), empty groups left out.
|
||||
*
|
||||
* @return array<string, array<string, array<string, mixed>>>
|
||||
*/
|
||||
public function read(): array
|
||||
{
|
||||
$data = [];
|
||||
foreach (GlobalData::get_global_variables() as $type => $items) {
|
||||
if ($type === 'colors' || (! is_array($items) && ! is_object($items))) {
|
||||
continue;
|
||||
}
|
||||
foreach ((array) $items as $id => $item) {
|
||||
// Divi's sanitizing keeps gvid- IDs only; the customizer fonts (--et_global_*_font) are not stored.
|
||||
if (! str_starts_with((string) $id, 'gvid-') || (! is_array($item) && ! is_object($item))) {
|
||||
continue;
|
||||
}
|
||||
$entry = (array) $item;
|
||||
unset($entry['allowedActions']);
|
||||
$data[(string) $type][(string) $id] = $entry;
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, array<string, array<string, mixed>>> $data The whole new inventory (stored form).
|
||||
* @param array<string, array<string, array<string, mixed>>> $expected The inventory the change was planned on.
|
||||
* @throws DiviLocked When variable-write is not released – before anything is written.
|
||||
* @throws RuntimeException When Divi refuses the inventory (stale, missing IDs, invalid entries).
|
||||
*/
|
||||
public function save(array $data, array $expected): void
|
||||
{
|
||||
ReleaseGate::assertFeature(DiviFeature::VariableWrite);
|
||||
$refusal = GlobalData::set_global_variables($data, $expected);
|
||||
if ($refusal instanceof WP_Error) {
|
||||
throw new RuntimeException('global variables refused: ' . $refusal->get_error_code() . ' '
|
||||
. $refusal->get_error_message());
|
||||
}
|
||||
}
|
||||
|
||||
/** Divi's role right "Variables Manager"; fail closed without Divi. */
|
||||
public function canManage(): bool
|
||||
{
|
||||
return function_exists('et_pb_is_allowed') && (bool) et_pb_is_allowed('variables_manager');
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,8 @@ 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\Design\GlobalColorsAdapter;
|
||||
use LIT\MCP\Integrations\Divi\Design\GlobalVariablesAdapter;
|
||||
use LIT\MCP\Integrations\Divi\Library\DiviLibraryStore;
|
||||
use LIT\MCP\Integrations\Divi\Library\DiviRoles;
|
||||
use LIT\MCP\Integrations\Divi\Pages\DiviPages;
|
||||
@@ -27,6 +29,8 @@ final class DiviAdapters
|
||||
DiviThemeBuilder::class,
|
||||
DesignSystemReader::class,
|
||||
DesignSystemWriter::class,
|
||||
GlobalColorsAdapter::class,
|
||||
GlobalVariablesAdapter::class,
|
||||
DiviModules::class,
|
||||
DynamicContentMap::class,
|
||||
];
|
||||
|
||||
@@ -21,17 +21,20 @@ enum DiviFeature: string
|
||||
case DynamicSources = 'dynamic-sources';
|
||||
case DesignRead = 'design-read';
|
||||
case PresetWrite = 'preset-write';
|
||||
case ColorWrite = 'color-write';
|
||||
case VariableWrite = 'variable-write';
|
||||
case LibraryRead = 'library-read';
|
||||
case LibraryWrite = 'library-write';
|
||||
case Roles = 'roles';
|
||||
case ThemeBuilderRead = 'tb-read';
|
||||
case ThemeBuilderWrite = 'tb-write';
|
||||
|
||||
/** Whether the feature changes data (the four *Write cases). */
|
||||
/** Whether the feature changes data (the six *Write cases). */
|
||||
public function isWrite(): bool
|
||||
{
|
||||
return match ($this) {
|
||||
self::PageWrite, self::PresetWrite, self::LibraryWrite, self::ThemeBuilderWrite => true,
|
||||
self::PageWrite, self::PresetWrite, self::ColorWrite, self::VariableWrite, self::LibraryWrite,
|
||||
self::ThemeBuilderWrite => true,
|
||||
default => false,
|
||||
};
|
||||
}
|
||||
@@ -46,6 +49,8 @@ enum DiviFeature: string
|
||||
self::DynamicSources => 'Dynamische Inhalte',
|
||||
self::DesignRead => 'Designsystem lesen',
|
||||
self::PresetWrite => 'Presets schreiben',
|
||||
self::ColorWrite => 'Farben schreiben',
|
||||
self::VariableWrite => 'Variablen schreiben',
|
||||
self::LibraryRead => 'Bibliothek lesen',
|
||||
self::LibraryWrite => 'Bibliothek schreiben',
|
||||
self::Roles => 'Divi-Rollen',
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
"sig:ET\\Builder\\Packages\\Conversion\\Conversion::get_preset_attrs_mapping": "b5c3403666ff92be1dd7bfa6e6589c32f6662b37282f2fd40922f8d50151745d",
|
||||
"sig:ET\\Builder\\Packages\\GlobalData\\GlobalData::get_global_colors": "dfbf01f6750e3fa6c309f1200effcfda539d75c3404732cd5df939a8bff399ae",
|
||||
"sig:ET\\Builder\\Packages\\GlobalData\\GlobalData::get_global_variables": "dfbf01f6750e3fa6c309f1200effcfda539d75c3404732cd5df939a8bff399ae",
|
||||
"sig:ET\\Builder\\Packages\\GlobalData\\GlobalData::sanitize_global_colors_data": "94f109150a8d6db57c309de00f4f4e369b4cd4a7e2a0682d5612dddeb65c8861",
|
||||
"sig:ET\\Builder\\Packages\\GlobalData\\GlobalData::set_global_colors": "d269cc06168a1898ea999e6df332449cf9bc04c17abefbb8f729a05136c836e6",
|
||||
"sig:ET\\Builder\\Packages\\GlobalData\\GlobalData::set_global_variables": "a2f422943530dba1afb5133459b25341dc67aa29e0be0a8626cbd60619c5a267",
|
||||
"sig:ET\\Builder\\Packages\\GlobalData\\GlobalPreset::get_data": "dfbf01f6750e3fa6c309f1200effcfda539d75c3404732cd5df939a8bff399ae",
|
||||
"sig:ET\\Builder\\Packages\\GlobalData\\GlobalPreset::get_deleted_preset_ids": "d36bfe7698116a94f7c6bc5ff00e3f66f3cd4267b68b5e61f0a5ec01e051b450",
|
||||
"sig:ET\\Builder\\Packages\\GlobalData\\GlobalPreset::get_group_preset_default_attr": "427ee58858537cf02a9d6dbbf58aafd779f8424c9c840806b5d7af8f25fbbda5",
|
||||
|
||||
Reference in New Issue
Block a user