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>
172 lines
5.8 KiB
PHP
172 lines
5.8 KiB
PHP
<?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;
|
||
}
|
||
}
|