feat: lit/divi-theme-options-update ändert Theme-Optionen über DesignChange (Prüfung, Divi-Sanitizer, Schriftvorschläge)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
122548db12
commit
879ca752e7
@@ -0,0 +1,246 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Design;
|
||||
|
||||
use LIT\MCP\Abilities\Ability;
|
||||
use LIT\MCP\Abilities\Annotations;
|
||||
use LIT\MCP\Abilities\Errors;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use LIT\MCP\Integrations\Divi\Release\RequiresDiviFeatures;
|
||||
use WP_Error;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* lit/divi-theme-options-update (spec §8.1, rulings W2–W5, W7): sets or resets the whitelisted Divi theme options
|
||||
* through the change pipeline (ThemeOptionsArea). set maps keys to values; null resets a key to Divi's default
|
||||
* (et_delete_option), "none" and false are refused. Every value passes ThemeOptionSpec::check() (type, range, choice),
|
||||
* fonts must be known to Divi exactly (FontCheck, with suggestions), and Divi's own sanitizer must keep the value –
|
||||
* false or a changed value is an error, since Divi would store false or round silently; the value is stored with the
|
||||
* sanitizer's type. A tablet/phone size equal to the size it inherits stays unset. All problems are collected with
|
||||
* the path set.<key>. Theme options have no page reference: the dry run warns about the whole website instead.
|
||||
*/
|
||||
final class DiviThemeOptionsUpdateAbility implements Ability, RequiresDiviFeatures
|
||||
{
|
||||
private const RESET_HINT = 'zum Zurücksetzen den Wert null angeben';
|
||||
|
||||
private const BUILDER = 'Geänderte Schriften wirken auch im Divi-Builder (Standardschrift der Module).';
|
||||
|
||||
public function __construct(
|
||||
private DesignSystemWriter $writer,
|
||||
private ThemeOptionsAdapter $options,
|
||||
private FontCatalog $fonts,
|
||||
private DesignChange $change
|
||||
) {
|
||||
}
|
||||
|
||||
public function name(): string
|
||||
{
|
||||
return 'lit/divi-theme-options-update';
|
||||
}
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return 'Divi-Theme-Optionen ändern';
|
||||
}
|
||||
|
||||
public function description(): string
|
||||
{
|
||||
return 'Ändert Divis Theme-Optionen für Schriften, Schriftgrößen und Layout. set: {Schlüssel: Wert}; null '
|
||||
. 'setzt auf den Divi-Standard zurück. Schlüssel, aktuelle Werte und Bereiche liefert '
|
||||
. 'lit/divi-theme-options-get; Schriften mit dem exakten Namen einer Divi-Schrift (unbekannte Namen '
|
||||
. 'bekommen Vorschläge). Die Hauptfarben ändert lit/divi-colors-set. Wirkt auf die gesamte Website: zuerst '
|
||||
. 'Probelauf (diff, Warnungen, design_rev), nach Rückfrage beim Nutzer confirm: true mit design_rev. '
|
||||
. 'Vorher wird ein Snapshot angelegt (lit/divi-design-restore).';
|
||||
}
|
||||
|
||||
public function inputSchema(): array
|
||||
{
|
||||
$keys = [];
|
||||
foreach (ThemeOptionSpec::KEYS as $key) {
|
||||
$keys[$key] = self::keySchema($key);
|
||||
}
|
||||
|
||||
return [
|
||||
'type' => 'object',
|
||||
'properties' => [
|
||||
'set' => [
|
||||
'type' => 'object',
|
||||
'minProperties' => 1,
|
||||
'properties' => $keys,
|
||||
'additionalProperties' => false,
|
||||
'description' => 'Theme-Option => neuer Wert; null = Divi-Standard.',
|
||||
],
|
||||
] + DesignChange::inputProperties(),
|
||||
'required' => ['set'],
|
||||
'additionalProperties' => false,
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(): ?array
|
||||
{
|
||||
return DesignChange::outputSchema();
|
||||
}
|
||||
|
||||
public function annotations(): array
|
||||
{
|
||||
return Annotations::destructive(false);
|
||||
}
|
||||
|
||||
public function isDirect(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Reads et_divi and the Customizer table (ThemeOptionRead); sanitizer, font list and writing: ThemeOptionWrite. */
|
||||
public function diviFeatures(array $input): array
|
||||
{
|
||||
return [DiviFeature::ThemeOptionRead, DiviFeature::ThemeOptionWrite];
|
||||
}
|
||||
|
||||
public function permission(array $input): bool|WP_Error
|
||||
{
|
||||
return DesignRights::themeOptions($this->writer, $this->options) ?? true;
|
||||
}
|
||||
|
||||
public function execute(array $input): mixed
|
||||
{
|
||||
$refusal = DesignRights::themeOptions($this->writer, $this->options);
|
||||
if ($refusal !== null) {
|
||||
return $refusal;
|
||||
}
|
||||
$set = $input['set'] ?? null;
|
||||
if (! is_array($set) || $set === [] || array_is_list($set)) {
|
||||
return Errors::invalidInput(
|
||||
'set fehlt oder ist leer; nichts wurde gespeichert.',
|
||||
['set' => 'Objekt {Theme-Option: Wert}, Schlüssel aus lit/divi-theme-options-get']
|
||||
);
|
||||
}
|
||||
|
||||
return $this->change->run(
|
||||
$this->name(),
|
||||
new ThemeOptionsArea($this->options),
|
||||
$input,
|
||||
fn (array $raw): DesignPlan|WP_Error => $this->plan(ThemeOptionBook::fromDivi($raw), $set)
|
||||
);
|
||||
}
|
||||
|
||||
/** @param array<array-key, mixed> $set */
|
||||
private function plan(ThemeOptionBook $before, array $set): DesignPlan|WP_Error
|
||||
{
|
||||
$errors = [];
|
||||
ItemKeys::refuseUnknown($set, ThemeOptionSpec::KEYS, 'set', $errors, self::RESET_HINT);
|
||||
$book = $before;
|
||||
// In the fixed key order, so a tablet size is compared with the desktop size of this call.
|
||||
foreach (ThemeOptionSpec::KEYS as $key) {
|
||||
if (! array_key_exists($key, $set)) {
|
||||
continue;
|
||||
}
|
||||
if ($set[$key] === null) {
|
||||
$book = $book->without($key);
|
||||
continue;
|
||||
}
|
||||
$value = $this->value($key, $set[$key], $errors);
|
||||
if ($value !== null && ! self::inherited($book, $key, $value)) {
|
||||
$book = $book->with($key, $value);
|
||||
}
|
||||
}
|
||||
if ($errors !== []) {
|
||||
return Errors::invalidInput('Ungültige Theme-Optionen; nichts wurde gespeichert.', $errors);
|
||||
}
|
||||
|
||||
$diff = ThemeOptionDiff::between($before, $book);
|
||||
|
||||
return new DesignPlan($book->data(), $diff, ThemeOptionDiff::summary($diff), null, self::warnings($diff));
|
||||
}
|
||||
|
||||
/**
|
||||
* The value Divi stores for $key, or null after noting the problem in $errors.
|
||||
*
|
||||
* @param array<string, string> $errors
|
||||
*/
|
||||
private function value(string $key, mixed $value, array &$errors): int|float|string|null
|
||||
{
|
||||
$field = 'set.' . $key;
|
||||
if ($value === false || $value === 'none') {
|
||||
$errors[$field] = 'für den Divi-Standard null angeben, nicht false oder "none"';
|
||||
|
||||
return null;
|
||||
}
|
||||
$problem = ThemeOptionSpec::check($key, $value);
|
||||
if ($problem === null && ThemeOptionSpec::definition($key)['type'] === 'font') {
|
||||
// Before Divi's sanitizer: it would only answer false, without suggestions.
|
||||
$problem = FontCheck::problem($this->fonts, (string) $value);
|
||||
}
|
||||
if ($problem !== null) {
|
||||
$errors[$field] = $problem;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$normalized = ThemeOptionSpec::normalize($key, $value);
|
||||
$sanitized = $this->options->sanitize($key, $normalized);
|
||||
if (! is_int($sanitized) && ! is_float($sanitized) && ! is_string($sanitized)) {
|
||||
$errors[$field] = 'Divi lehnt den Wert ab';
|
||||
|
||||
return null;
|
||||
}
|
||||
if (! ThemeOptionsArea::same($sanitized, $normalized)) {
|
||||
$errors[$field] = sprintf('Divi würde den Wert verändert speichern (%s)', $sanitized);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return $sanitized;
|
||||
}
|
||||
|
||||
/** Ruling W3: an unset tablet/phone size given the size it inherits stays unset – it keeps following that size. */
|
||||
private static function inherited(ThemeOptionBook $book, string $key, int|float|string $value): bool
|
||||
{
|
||||
return isset(ThemeOptionSpec::definition($key)['inherits']) && ! $book->has($key)
|
||||
&& ThemeOptionsArea::same(ThemeOptionSpec::effective($key, $book->data()), $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ruling W7: no affected pages, but the reach of the change.
|
||||
*
|
||||
* @param list<array<string, mixed>> $diff
|
||||
* @return list<string>
|
||||
*/
|
||||
private static function warnings(array $diff): array
|
||||
{
|
||||
if ($diff === []) {
|
||||
return [];
|
||||
}
|
||||
foreach ($diff as $row) {
|
||||
if (ThemeOptionSpec::definition((string) $row['key'])['type'] === 'font') {
|
||||
return [ThemeOptionsArea::SITE_WIDE, self::BUILDER];
|
||||
}
|
||||
}
|
||||
|
||||
return [ThemeOptionsArea::SITE_WIDE];
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> The schema of one key: its types, the range of numbers, a description. */
|
||||
private static function keySchema(string $key): array
|
||||
{
|
||||
$definition = ThemeOptionSpec::definition($key);
|
||||
$schema = ['type' => match ($definition['type']) {
|
||||
'int' => ['integer', 'string', 'null'],
|
||||
'float' => ['number', 'string', 'null'],
|
||||
'weight' => ['string', 'integer', 'null'],
|
||||
default => ['string', 'null'],
|
||||
}];
|
||||
if (isset($definition['min'], $definition['max'])) {
|
||||
$schema['minimum'] = $definition['min'];
|
||||
$schema['maximum'] = $definition['max'];
|
||||
}
|
||||
$schema['description'] = $definition['label'] . (isset($definition['choices'])
|
||||
? ': ' . implode(', ', $definition['choices'])
|
||||
: '');
|
||||
|
||||
return $schema;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Design;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* Ruling W5: a font name is valid when Divi knows it exactly (FontCatalog); otherwise the message names up to five
|
||||
* similar fonts (FontSuggest) or the upload tool. Shared by the theme options and the font variables (W12).
|
||||
*/
|
||||
final class FontCheck
|
||||
{
|
||||
/** @return string|null Null when $name is a font Divi knows, else a German message. */
|
||||
public static function problem(FontCatalog $fonts, string $name): ?string
|
||||
{
|
||||
if ($fonts->has($name)) {
|
||||
return null;
|
||||
}
|
||||
$similar = FontSuggest::similar($name, $fonts->names());
|
||||
|
||||
return sprintf('unbekannte Schrift „%s“', $name) . ($similar === []
|
||||
? '; eigene Schriften lädt lit/divi-fonts-upload hoch'
|
||||
: '; ähnlich: ' . implode(', ', $similar));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user