From 5bb25e1e8f5d22092e456a0c1375eb5d0fac91d6 Mon Sep 17 00:00:00 2001 From: Jan-Niclas Loosen Date: Mon, 19 Oct 2026 13:59:33 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20VariableValue=20pr=C3=BCft=20Variablenw?= =?UTF-8?q?erte=20je=20Typ=20(numbers,=20strings,=20images,=20links,=20fon?= =?UTF-8?q?ts)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- .../Divi/Design/VariableValue.php | 100 +++++++++++++++ .../Divi/Design/VariableValueTest.php | 115 ++++++++++++++++++ 2 files changed, 215 insertions(+) create mode 100644 src/Integrations/Divi/Design/VariableValue.php create mode 100644 tests/Unit/Integrations/Divi/Design/VariableValueTest.php diff --git a/src/Integrations/Divi/Design/VariableValue.php b/src/Integrations/Divi/Design/VariableValue.php new file mode 100644 index 0000000..a9e5962 --- /dev/null +++ b/src/Integrations/Divi/Design/VariableValue.php @@ -0,0 +1,100 @@ +"]+|\/[^\s<>"]*|mailto:[^\s<>"]+|tel:[^\s<>"]+)$/iD'; + + private const WEB_URL = '/^https?:\/\/[^\s<>"]+$/iD'; + + /** @return string|null Null when $value is valid for $type, else a German message. */ + public static function check(string $type, string $value): ?string + { + if (! in_array($type, self::TYPES, true)) { + return sprintf('Typ "%s" ist nicht erlaubt; erlaubt: %s.', $type, implode(', ', self::TYPES)); + } + if ($value === '') { + return sprintf('Wert fehlt (Typ "%s").', $type); + } + if (! mb_check_encoding($value, 'UTF-8')) { + return 'Wert ist kein gültiges UTF-8.'; + } + + return match ($type) { + 'numbers' => self::numbers($value), + 'strings' => self::strings($value), + 'links' => self::links($value), + 'images' => self::images($value), + default => self::font($value), + }; + } + + private static function numbers(string $value): ?string + { + if (mb_strlen($value) > self::NUMBERS_MAX || preg_match('/[;{}<>\x00-\x1F\x7F]/', $value) === 1) { + return 'Zahlenwert: ein CSS-Ausdruck mit höchstens 200 Zeichen ohne ; { } < > und Zeilenumbruch ' + . '(z. B. 8px oder clamp(1.5rem, 4vw, 2.5rem)).'; + } + + return null; + } + + private static function strings(string $value): ?string + { + if (mb_strlen($value) > self::STRINGS_MAX || preg_match(self::CONTROL, $value) === 1) { + return 'Textwert: höchstens 2000 Zeichen, keine Steuerzeichen außer dem Zeilenumbruch.'; + } + + return null; + } + + private static function links(string $value): ?string + { + if (mb_strlen($value) > self::URL_MAX || preg_match(self::URL, $value) !== 1) { + return 'Link: eine http(s)://- oder /-URL, mailto: oder tel: mit höchstens 2000 Zeichen.'; + } + + return null; + } + + private static function images(string $value): ?string + { + $valid = preg_match('/^\d+$/D', $value) === 1 + || (mb_strlen($value) <= self::URL_MAX && preg_match(self::WEB_URL, $value) === 1); + + return $valid ? null : 'Bild: eine Anhang-ID (Ziffern) oder eine http(s)://-URL mit höchstens 2000 Zeichen.'; + } + + private static function font(string $value): ?string + { + if (mb_strlen($value) > self::FONT_MAX || preg_match('/[;{}<>"\'\\\\\x00-\x1F\x7F]/', $value) === 1) { + return 'Schrift: ein Schriftname mit höchstens 100 Zeichen ohne ; { } < > " \' \\.'; + } + + return null; + } +} diff --git a/tests/Unit/Integrations/Divi/Design/VariableValueTest.php b/tests/Unit/Integrations/Divi/Design/VariableValueTest.php new file mode 100644 index 0000000..8c2962a --- /dev/null +++ b/tests/Unit/Integrations/Divi/Design/VariableValueTest.php @@ -0,0 +1,115 @@ + */ + public static function validValues(): array + { + return [ + 'numbers px' => ['numbers', '8px'], + 'numbers clamp' => ['numbers', 'clamp(1.5rem, 4vw, 2.5rem)'], + 'numbers calc' => ['numbers', 'calc(100% - var(--gvid-f6yq6mayfy))'], + 'strings' => ['strings', 'Handwerk aus Meisterhand – seit 1987'], + 'strings with line feed' => ['strings', "Zeile 1\nZeile 2"], + 'strings with quotes and markup chars' => ['strings', '"Hallo" & '], + 'links https' => ['links', 'https://wordpress.ddev.site:8443/kontakt/'], + 'links http' => ['links', 'http://example.com'], + 'links relative' => ['links', '/kontakt/#formular'], + 'links mailto' => ['links', 'mailto:info@example.com'], + 'links tel' => ['links', 'tel:+49221123'], + 'images id' => ['images', '1234'], + 'images url' => ['images', 'https://example.com/a/600.jpg'], + 'fonts' => ['fonts', 'Merriweather'], + 'fonts with space and digits' => ['fonts', 'Source Sans 3'], + ]; + } + + /** @dataProvider validValues */ + public function testValidValues(string $type, string $value): void + { + self::assertNull(VariableValue::check($type, $value)); + } + + /** @return array */ + public static function invalidValues(): array + { + return [ + 'numbers empty' => ['numbers', ''], + 'numbers semicolon' => ['numbers', '8px; color: red'], + 'numbers braces' => ['numbers', '8px}'], + 'numbers angle' => ['numbers', '<8px>'], + 'numbers line feed' => ['numbers', "8px\n9px"], + 'numbers too long' => ['numbers', str_repeat('1', 201)], + 'strings empty' => ['strings', ''], + 'strings control' => ['strings', "a\x07b"], + 'strings tab' => ['strings', "a\tb"], + 'strings carriage return' => ['strings', "a\r\nb"], + 'strings too long' => ['strings', str_repeat('ä', 2001)], + 'strings invalid utf8' => ['strings', "a\xC3\x28"], + 'links empty' => ['links', ''], + 'links javascript' => ['links', 'javascript:alert(1)'], + 'links data' => ['links', 'data:text/html,x'], + 'links anchor only' => ['links', '#kontakt'], + 'links relative without slash' => ['links', 'kontakt/'], + 'links with space' => ['links', 'https://example.com/a b'], + 'links mailto empty' => ['links', 'mailto:'], + 'links too long' => ['links', 'https://example.com/' . str_repeat('a', 2000)], + 'images negative' => ['images', '-5'], + 'images relative path' => ['images', '/wp-content/a.jpg'], + 'images text' => ['images', 'Werkstattbild'], + 'images javascript' => ['images', 'javascript:alert(1)'], + 'fonts empty' => ['fonts', ''], + 'fonts semicolon' => ['fonts', 'Lato; color: red'], + 'fonts quote' => ['fonts', "Lato'"], + 'fonts backslash' => ['fonts', 'Lato\\'], + 'fonts too long' => ['fonts', str_repeat('a', 101)], + ]; + } + + /** @dataProvider invalidValues */ + public function testInvalidValuesGetAGermanMessage(string $type, string $value): void + { + $message = VariableValue::check($type, $value); + + self::assertIsString($message); + self::assertNotSame('', $message); + } + + public function testBoundsAreInclusive(): void + { + self::assertNull(VariableValue::check('numbers', str_repeat('1', 200))); + self::assertNull(VariableValue::check('strings', str_repeat('ä', 2000))); + self::assertNull(VariableValue::check('fonts', str_repeat('a', 100))); + self::assertNull(VariableValue::check('links', 'https://example.com/' . str_repeat('a', 1980))); + } + + public function testGradientsAndUnknownTypesAreNotCreatable(): void + { + self::assertSame( + 'Typ "gradients" ist nicht erlaubt; erlaubt: numbers, strings, images, links, fonts.', + VariableValue::check('gradients', 'linear-gradient(red, blue)') + ); + self::assertSame( + 'Typ "colors" ist nicht erlaubt; erlaubt: numbers, strings, images, links, fonts.', + VariableValue::check('colors', '#fff') + ); + self::assertSame(['numbers', 'strings', 'images', 'links', 'fonts'], VariableValue::TYPES); + } + + public function testMessagesNameTheTypeOrTheRule(): void + { + self::assertSame('Wert fehlt (Typ "fonts").', VariableValue::check('fonts', '')); + self::assertStringStartsWith('Zahlenwert:', (string) VariableValue::check('numbers', '1;')); + self::assertStringStartsWith('Link:', (string) VariableValue::check('links', 'x')); + self::assertStringStartsWith('Bild:', (string) VariableValue::check('images', 'x')); + self::assertStringStartsWith('Schrift:', (string) VariableValue::check('fonts', 'a{')); + self::assertStringStartsWith('Textwert:', (string) VariableValue::check('strings', "a\x00")); + } +}