Files
WP-LIT-MCP/src/Integrations/Divi/Design/DiviDesignRestoreAbility.php
T
Jan-Niclas LoosenandClaude Sonnet 5.5 ac333a164c feat(css): globales CSS bearbeiten (lit/divi-custom-css-get und -update)
WordPress „Zusätzliches CSS“ (in Divi „Eigenes CSS“) über die Design-Pipeline: Probelauf mit Zeilen-Diff,
design_rev, Bestätigung, Snapshot, Rollback und Wiederherstellen (Bereich custom_css). Der Guard lehnt </style,
<script, expression(, javascript: und neue @import ab und warnt bei fremden Hosts; nur Neues zählt, Bestehendes
blockiert keine Änderung. Erfordert edit_css und die Divi-Rolle Theme-Anpassung.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-05 16:36:16 +01:00

172 lines
5.8 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 LIT\MCP\Abilities\Ability;
use LIT\MCP\Abilities\Annotations;
use LIT\MCP\Abilities\Errors;
use LIT\MCP\Integrations\Divi\Release\FeatureSets;
use LIT\MCP\Integrations\Divi\Release\RequiresDiviFeatures;
use WP_Error;
defined('ABSPATH') || exit;
/**
* lit/divi-design-restore (spec §5, G6, ruling V10): writes a snapshot back through the change pipeline – dry run with
* the diff to the current state, confirm with design_rev, and first a snapshot of the current state, so a restore can
* be undone as well. The area comes from the snapshot ID; its RestorePlanner brings the rights, the Divi features and
* the plan. A snapshot of an area without a planner is refused as unsupported, never treated as a no-op.
*/
final class DiviDesignRestoreAbility implements Ability, RequiresDiviFeatures
{
private const ID = '/^(colors|variables|presets|theme_options|custom_css)-\d{8}T\d{6}Z-[0-9a-f]{6}$/D';
/** @var array<string, RestorePlanner> area => planner */
private array $planners = [];
public function __construct(
private DesignChange $change,
private SnapshotStore $snapshots,
RestorePlanner ...$planners
) {
foreach ($planners as $planner) {
$this->planners[$planner->area()] = $planner;
}
}
public function name(): string
{
return 'lit/divi-design-restore';
}
public function label(): string
{
return 'Designsystem wiederherstellen';
}
public function description(): string
{
return 'Stellt einen Snapshot des Designsystems wieder her (snapshot_id aus lit/divi-design-snapshots oder '
. 'einer Änderungsantwort). Zuerst Probelauf (Unterschied zum aktuellen Stand, design_rev), nach Rückfrage '
. 'beim Nutzer confirm: true; vorher wird der aktuelle Stand selbst gesichert. Bereiche: presets, colors '
. '(Farben, die es im Snapshot nicht gibt, entfallen), variables (Variablen, die erst nach dem Snapshot '
. 'entstanden, bleiben archiviert erhalten) und theme_options (Theme-Optionen, die im Snapshot nicht '
. 'gesetzt sind, kehren zum Divi-Standard zurück) und custom_css (das ganze eigene CSS, nur für dasselbe '
. 'Theme).';
}
public function inputSchema(): array
{
return [
'type' => 'object',
'properties' => ['snapshot_id' => ['type' => 'string']] + DesignChange::inputProperties(),
'required' => ['snapshot_id'],
'additionalProperties' => false,
];
}
public function outputSchema(): ?array
{
return DesignChange::outputSchema();
}
public function annotations(): array
{
return Annotations::destructive(false);
}
public function isDirect(): bool
{
return false;
}
/** The features of the snapshot's area; without a restorable area those of every area (fail closed). */
public function diviFeatures(array $input): array
{
return FeatureSets::merge(...array_map(
static fn (RestorePlanner $planner): array => $planner->features(),
$this->plannersFor($input)
));
}
public function permission(array $input): bool|WP_Error
{
return $this->rights($input) ?? true;
}
public function execute(array $input): mixed
{
$refusal = $this->rights($input);
if ($refusal !== null) {
return $refusal;
}
$id = is_string($input['snapshot_id'] ?? null) ? $input['snapshot_id'] : '';
$area = self::areaOf($input);
if ($area === null) {
return Errors::invalidInput(
'Ungültige Snapshot-ID.',
['snapshot_id' => 'z. B. presets-20261016T101500Z-a1b2c3']
);
}
$planner = $this->planners[$area] ?? null;
if ($planner === null) {
return Errors::unsupported(
'Snapshots dieses Bereichs lassen sich hier nicht wiederherstellen.',
'Den Stand des Snapshots im Divi-Builder bzw. -Customizer von Hand herstellen.'
);
}
$snapshot = $this->snapshots->get($area, $id);
if ($snapshot === null) {
return Errors::notFound(
sprintf('Snapshot %s gibt es nicht (mehr).', $id),
'Vorhandene Snapshots mit lit/divi-design-snapshots auflisten; je Bereich bleiben die letzten 20.'
);
}
return $this->change->run(
$this->name(),
$planner->designArea(),
$input,
static fn (array $raw): DesignPlan|WP_Error => $planner->plan($raw, $snapshot['data'], $id)
);
}
/**
* Spec §10: the rights of the snapshot's area; without a restorable area those of every area (fail closed).
*
* @param array<string, mixed> $input
*/
private function rights(array $input): ?WP_Error
{
foreach ($this->plannersFor($input) as $planner) {
$refusal = $planner->rights();
if ($refusal !== null) {
return $refusal;
}
}
return null;
}
/**
* @param array<string, mixed> $input
* @return list<RestorePlanner> The planner of the snapshot's area, or every planner.
*/
private function plannersFor(array $input): array
{
$planner = $this->planners[self::areaOf($input) ?? ''] ?? null;
return $planner !== null ? [$planner] : array_values($this->planners);
}
/** @param array<string, mixed> $input */
private static function areaOf(array $input): ?string
{
$id = $input['snapshot_id'] ?? null;
return is_string($id) && preg_match(self::ID, $id, $match) === 1 ? $match[1] : null;
}
}