diff --git a/src/Integrations/Divi/Design/DiviColorsSetAbility.php b/src/Integrations/Divi/Design/DiviColorsSetAbility.php index 0baec8b..753df7d 100644 --- a/src/Integrations/Divi/Design/DiviColorsSetAbility.php +++ b/src/Integrations/Divi/Design/DiviColorsSetAbility.php @@ -99,6 +99,7 @@ final class DiviColorsSetAbility implements Ability, RequiresDiviFeatures 'maxItems' => self::MAX_ITEMS, 'items' => [ 'type' => 'object', + 'additionalProperties' => false, 'properties' => [ 'id' => [ 'type' => 'string', @@ -226,6 +227,13 @@ final class DiviColorsSetAbility implements Ability, RequiresDiviFeatures private function apply(ColorBook $book, array $item, string $field, array &$errors): ColorBook { $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; if (count($errors) > $known) { return $book; diff --git a/src/Integrations/Divi/Design/DiviVariablesSetAbility.php b/src/Integrations/Divi/Design/DiviVariablesSetAbility.php index 0ed7d36..eab2c51 100644 --- a/src/Integrations/Divi/Design/DiviVariablesSetAbility.php +++ b/src/Integrations/Divi/Design/DiviVariablesSetAbility.php @@ -87,6 +87,7 @@ final class DiviVariablesSetAbility implements Ability, RequiresDiviFeatures 'maxItems' => self::MAX_ITEMS, 'items' => [ 'type' => 'object', + 'additionalProperties' => false, 'properties' => [ 'id' => ['type' => 'string', 'description' => 'Vorhandene Variable: ID oder Name.'], 'type' => [ @@ -201,6 +202,13 @@ final class DiviVariablesSetAbility implements Ability, RequiresDiviFeatures private function apply(VariableBook $book, array $item, string $field, array &$errors): VariableBook { $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); $found = $named ? self::existing($book, $item, $field, $errors) : null; $type = $found['type'] ?? ($named ? null : self::newType($item, $field, $errors)); diff --git a/src/Integrations/Divi/Design/ItemKeys.php b/src/Integrations/Divi/Design/ItemKeys.php new file mode 100644 index 0000000..1facc73 --- /dev/null +++ b/src/Integrations/Divi/Design/ItemKeys.php @@ -0,0 +1,23 @@ + $item + * @param list $allowed + * @param array $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'; + } + } +} diff --git a/tests/Unit/Integrations/Divi/Design/DiviColorsSetAbilityTest.php b/tests/Unit/Integrations/Divi/Design/DiviColorsSetAbilityTest.php index 9e3e1f5..1f53657 100644 --- a/tests/Unit/Integrations/Divi/Design/DiviColorsSetAbilityTest.php +++ b/tests/Unit/Integrations/Divi/Design/DiviColorsSetAbilityTest.php @@ -276,6 +276,23 @@ final class DiviColorsSetAbilityTest extends TestCase 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 { $calls = []; diff --git a/tests/Unit/Integrations/Divi/Design/DiviVariablesSetAbilityTest.php b/tests/Unit/Integrations/Divi/Design/DiviVariablesSetAbilityTest.php index f2494a7..6bbf9d0 100644 --- a/tests/Unit/Integrations/Divi/Design/DiviVariablesSetAbilityTest.php +++ b/tests/Unit/Integrations/Divi/Design/DiviVariablesSetAbilityTest.php @@ -275,6 +275,24 @@ final class DiviVariablesSetAbilityTest extends TestCase 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). */ public function testEmptyAndZeroLabelsAreRefused(): void {