fix: lit/divi-colors-set und lit/divi-variables-set lehnen unbekannte Item-Felder ab (delete, Tippfehler)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Jan-Niclas Loosen
2026-10-19 14:38:00 +02:00
co-authored by Claude Sonnet 5.5
parent bb2377991d
commit 02ed5bdbd3
5 changed files with 74 additions and 0 deletions
@@ -99,6 +99,7 @@ final class DiviColorsSetAbility implements Ability, RequiresDiviFeatures
'maxItems' => self::MAX_ITEMS, 'maxItems' => self::MAX_ITEMS,
'items' => [ 'items' => [
'type' => 'object', 'type' => 'object',
'additionalProperties' => false,
'properties' => [ 'properties' => [
'id' => [ 'id' => [
'type' => 'string', 'type' => 'string',
@@ -226,6 +227,13 @@ final class DiviColorsSetAbility implements Ability, RequiresDiviFeatures
private function apply(ColorBook $book, array $item, string $field, array &$errors): ColorBook private function apply(ColorBook $book, array $item, string $field, array &$errors): ColorBook
{ {
$known = count($errors); $known = count($errors);
ItemKeys::refuseUnknown(
$item,
['id', 'label', 'color', 'relative', 'folder', 'status'],
$field,
$errors,
'Farben werden nicht gelöscht, nur deaktiviert (status: inactive)'
);
$id = array_key_exists('id', $item) ? self::existing($book, $item['id'], $field . '.id', $errors) : null; $id = array_key_exists('id', $item) ? self::existing($book, $item['id'], $field . '.id', $errors) : null;
if (count($errors) > $known) { if (count($errors) > $known) {
return $book; return $book;
@@ -87,6 +87,7 @@ final class DiviVariablesSetAbility implements Ability, RequiresDiviFeatures
'maxItems' => self::MAX_ITEMS, 'maxItems' => self::MAX_ITEMS,
'items' => [ 'items' => [
'type' => 'object', 'type' => 'object',
'additionalProperties' => false,
'properties' => [ 'properties' => [
'id' => ['type' => 'string', 'description' => 'Vorhandene Variable: ID oder Name.'], 'id' => ['type' => 'string', 'description' => 'Vorhandene Variable: ID oder Name.'],
'type' => [ 'type' => [
@@ -201,6 +202,13 @@ final class DiviVariablesSetAbility implements Ability, RequiresDiviFeatures
private function apply(VariableBook $book, array $item, string $field, array &$errors): VariableBook private function apply(VariableBook $book, array $item, string $field, array &$errors): VariableBook
{ {
$known = count($errors); $known = count($errors);
ItemKeys::refuseUnknown(
$item,
['id', 'type', 'label', 'value', 'status'],
$field,
$errors,
'Variablen werden nicht gelöscht, nur archiviert (status: archived)'
);
$named = array_key_exists('id', $item); $named = array_key_exists('id', $item);
$found = $named ? self::existing($book, $item, $field, $errors) : null; $found = $named ? self::existing($book, $item, $field, $errors) : null;
$type = $found['type'] ?? ($named ? null : self::newType($item, $field, $errors)); $type = $found['type'] ?? ($named ? null : self::newType($item, $field, $errors));
+23
View File
@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Design;
defined('ABSPATH') || exit;
/** Refuses item keys a tool does not know, so "delete" or a typo is never taken for a successful no-op. */
final class ItemKeys
{
/**
* @param array<array-key, mixed> $item
* @param list<string> $allowed
* @param array<string, string> $errors gets one entry per unknown key (field.key)
*/
public static function refuseUnknown(array $item, array $allowed, string $field, array &$errors, string $deleteHint): void
{
foreach (array_diff(array_map('strval', array_keys($item)), $allowed) as $key) {
$errors[$field . '.' . $key] = $key === 'delete' ? 'unbekanntes Feld; ' . $deleteHint : 'unbekanntes Feld';
}
}
}
@@ -276,6 +276,23 @@ final class DiviColorsSetAbilityTest extends TestCase
self::assertSame([], $this->saves); self::assertSame([], $this->saves);
} }
public function testUnknownItemKeysAreRefusedAndCollectedWithOtherErrors(): void
{
$error = $this->ability()->execute(['items' => [
['id' => 'gcid-holz000001', 'delete' => true],
['id' => 'gcid-holz000001', 'stauts' => 'inactive'],
['id' => 'Fehlt', 'color' => '#ffffff'],
]]);
self::assertToolError($error, 'validation_failed');
$fields = self::errorFields($error);
self::assertSame(['items[0].delete', 'items[1].stauts', 'items[2].id'], array_keys($fields));
self::assertStringContainsString('inactive', $fields['items[0].delete']);
self::assertSame('unbekanntes Feld', $fields['items[1].stauts']);
self::assertSame([], $this->saves);
self::assertFalse($this->ability()->inputSchema()['properties']['items']['items']['additionalProperties']);
}
public function testAFailedWriteIsRolledBack(): void public function testAFailedWriteIsRolledBack(): void
{ {
$calls = []; $calls = [];
@@ -275,6 +275,24 @@ final class DiviVariablesSetAbilityTest extends TestCase
self::assertSame([], $this->saves); self::assertSame([], $this->saves);
} }
/** Ruling V1: "delete" (or a typo) must not look like a successful no-op. */
public function testUnknownItemKeysAreRefusedAndCollectedWithOtherErrors(): void
{
$error = $this->ability()->execute(['items' => [
['id' => 'Slogan', 'delete' => true],
['id' => 'Slogan', 'stauts' => 'archived'],
['id' => 'Fehlt', 'value' => '1px'],
]]);
self::assertToolError($error, 'validation_failed');
$fields = self::errorFields($error);
self::assertSame(['items[0].delete', 'items[1].stauts', 'items[2].id'], array_keys($fields));
self::assertStringContainsString('archiviert', $fields['items[0].delete']);
self::assertSame('unbekanntes Feld', $fields['items[1].stauts']);
self::assertSame([], $this->saves);
self::assertFalse($this->ability()->inputSchema()['properties']['items']['items']['additionalProperties']);
}
/** Divi silently drops variables with an empty label or the label "0" (sanitize_global_variables_data). */ /** Divi silently drops variables with an empty label or the label "0" (sanitize_global_variables_data). */
public function testEmptyAndZeroLabelsAreRefused(): void public function testEmptyAndZeroLabelsAreRefused(): void
{ {