feat: VariableValue prüft Variablenwerte je Typ (numbers, strings, images, links, fonts)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
39a5782562
commit
5bb25e1e8f
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Design;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* Checks the value of a global variable before it goes to Divi (ruling V6, D7 principle). The type "gradients"
|
||||
* (new in Divi 5.15) is passed through by VariableBook, but the tools never create or change it. The check of fonts
|
||||
* against Divi's font list follows with 4d. Pure PHP.
|
||||
*/
|
||||
final class VariableValue
|
||||
{
|
||||
/** Types the tools may create (V6). */
|
||||
public const TYPES = ['numbers', 'strings', 'images', 'links', 'fonts'];
|
||||
|
||||
private const NUMBERS_MAX = 200;
|
||||
|
||||
private const STRINGS_MAX = 2000;
|
||||
|
||||
private const URL_MAX = 2000;
|
||||
|
||||
private const FONT_MAX = 100;
|
||||
|
||||
/** Control characters except the line feed. */
|
||||
private const CONTROL = '/[\x00-\x09\x0B-\x1F\x7F]/';
|
||||
|
||||
private const URL = '/^(?:https?:\/\/[^\s<>"]+|\/[^\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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Design;
|
||||
|
||||
use LIT\MCP\Integrations\Divi\Design\VariableValue;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class VariableValueTest extends TestCase
|
||||
{
|
||||
/** @return array<string, array{string, string}> */
|
||||
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" & <willkommen>'],
|
||||
'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<string, array{string, string}> */
|
||||
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"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user