refactor: ValueKind enum instead of kind string literals (L1)
enum ValueKind: string { Html, Code, Url, Text } with strictness() and
tightens(self $routed) (the S33a rule formerly AttrWriter/ValueCleaner::tightens
over STRICTNESS). ValueCleaner::clean() takes ?ValueKind, kindOf()/kind()/
unknownKind() return it, the sanitizer match is exhaustive over the four cases;
the "Feldart" message prints ->value, so its text is unchanged. ShortformWriter
drops the identity map CLEAN_KIND for ValueKind::tryFrom($spec['kind']) ??
ValueKind::Text; Composer and ShortformWriter pass ValueKind::Text/Url.
The installed PHPCompatibility sniffs do not know enums: two sniffs are disabled
in ValueKind.php only, with the reason.
Tests: ValueCleanerTest passes/asserts ValueKind cases instead of strings;
+ValueKindTest (full tightens matrix, case values).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
This commit is contained in:
@@ -20,7 +20,8 @@ use LIT\MCP\Integrations\Divi\Schema\ModuleCatalog;
|
||||
* 3. UTF-8 check of the raw string.
|
||||
* 4. S32/S32a: ControlChars::strip() on the RAW string.
|
||||
* 5. S3/S3a/S23/S24: kind routing by catalog kind, sub-field, key name, path segment
|
||||
* (kind()); S33a: a kind override may only tighten the routed kind (tightens()).
|
||||
* (kind(), a ValueKind); S33a: a kind override may only tighten the routed kind
|
||||
* (ValueKind::tightens()).
|
||||
* 6. S1/S2: a string with the $variable( marker → VariableRef::canonical() (whole value only;
|
||||
* URL fields only content references) – its result is returned as it is.
|
||||
* 7. the sanitizer of the kind: html / code / url / text (ValueSanitizer).
|
||||
@@ -48,9 +49,6 @@ final class ValueCleaner
|
||||
/** Last path segment naming a URL when neither kind nor key does (ruling S24): redirectUrl. */
|
||||
private const URL_SEGMENT = '/url$/i';
|
||||
|
||||
/** Text-family kinds from laxest to strictest (ruling S33a); url stands apart. */
|
||||
private const STRICTNESS = ['code' => 0, 'html' => 1, 'text' => 2];
|
||||
|
||||
/**
|
||||
* Raw HTML/CSS fields, and the ones Divi re-serializes whole pages for (rulings S7/S26), by
|
||||
* path shape at any depth: module.advanced.html(.htmlBefore), module.decoration.attributes,
|
||||
@@ -68,12 +66,11 @@ final class ValueCleaner
|
||||
/**
|
||||
* Cleans one value written to $path (steps 1–8 above). $subKey names the sub-field of a
|
||||
* composite path ("linkUrl" of button.innerContent) when $value is its string alone. $kind
|
||||
* ('html'|'code'|'url'|'text') replaces the catalog routing for every string of $value: the
|
||||
* short form passes its curated field kind (ShortformMap), so icon-list-item text is always
|
||||
* plain text. It may only tighten (ruling S33a): code < html < text; url only where the
|
||||
* routing says url – a weaker or other-family kind is refused.
|
||||
* replaces the catalog routing for every string of $value: the short form passes its
|
||||
* curated field kind (ShortformMap), so icon-list-item text is always plain text. It may only
|
||||
* tighten (ruling S33a): code < html < text; url only where the routing says url – a weaker
|
||||
* or other-family kind is refused.
|
||||
*
|
||||
* @param 'html'|'code'|'url'|'text'|null $kind
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function clean(
|
||||
@@ -81,7 +78,7 @@ final class ValueCleaner
|
||||
string $path,
|
||||
mixed $value,
|
||||
?string $subKey = null,
|
||||
?string $kind = null
|
||||
?ValueKind $kind = null
|
||||
): mixed {
|
||||
$this->guardPath($path);
|
||||
|
||||
@@ -91,10 +88,8 @@ final class ValueCleaner
|
||||
/**
|
||||
* The kind clean() routes a string of $path (sub-field $subKey) to. lit/divi-replace-text
|
||||
* uses it to leave URL and code fields alone (D10).
|
||||
*
|
||||
* @return 'html'|'code'|'url'|'text'
|
||||
*/
|
||||
public function kindOf(string $module, string $path, ?string $subKey = null): string
|
||||
public function kindOf(string $module, string $path, ?string $subKey = null): ValueKind
|
||||
{
|
||||
return $this->kind($module, $path, $subKey, $subKey);
|
||||
}
|
||||
@@ -111,7 +106,7 @@ final class ValueCleaner
|
||||
return function (string $path, ?string $key, string $value) use ($module): ?string {
|
||||
$kind = $this->kindOf($module, $path, $key);
|
||||
|
||||
return $kind === 'text' || $kind === 'html'
|
||||
return $kind === ValueKind::Text || $kind === ValueKind::Html
|
||||
? (string) $this->clean($module, $path, $value, $key)
|
||||
: null;
|
||||
};
|
||||
@@ -139,7 +134,7 @@ final class ValueCleaner
|
||||
*
|
||||
* @param string|null $subName First key below the state value: selects the sub-kind of a composite path.
|
||||
* @param string|null $key Nearest key above $value: the URL fallback when the catalog names no kind.
|
||||
* @param 'html'|'code'|'url'|'text'|null $kind Fixed kind instead of the routing (short form).
|
||||
* @param ValueKind|null $kind Fixed kind instead of the routing (short form).
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
private function cleanNode(
|
||||
@@ -148,7 +143,7 @@ final class ValueCleaner
|
||||
mixed $value,
|
||||
?string $subName,
|
||||
?string $key,
|
||||
?string $kind = null
|
||||
?ValueKind $kind = null
|
||||
): mixed {
|
||||
if (is_array($value)) {
|
||||
$clean = [];
|
||||
@@ -185,27 +180,27 @@ final class ValueCleaner
|
||||
|
||||
// Step 5 (S3/S3a/S23/S24, S33a).
|
||||
$routed = $this->kind($module, $path, $subName, $key);
|
||||
if ($kind !== null && ! self::tightens($kind, $routed)) {
|
||||
if ($kind !== null && ! $kind->tightens($routed)) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'Feldart "%s" ist für "%s" nicht zulässig (das Attribut verlangt "%s").',
|
||||
$kind,
|
||||
$kind->value,
|
||||
$path,
|
||||
$routed
|
||||
$routed->value
|
||||
));
|
||||
}
|
||||
|
||||
$kind ??= $routed;
|
||||
// Step 6 (S1/S2): the only way a reference is stored – whole, straight from canonical().
|
||||
if (VariableRef::contains($value)) {
|
||||
return $this->references->canonical($value, $this->sanitizer, $kind === 'url');
|
||||
return $this->references->canonical($value, $this->sanitizer, $kind === ValueKind::Url);
|
||||
}
|
||||
|
||||
// Step 7.
|
||||
$clean = match ($kind) {
|
||||
'html' => $this->sanitizer->html($value),
|
||||
'code' => $this->sanitizer->code($value),
|
||||
'url' => $this->sanitizer->url($value),
|
||||
default => $this->sanitizer->text($value),
|
||||
ValueKind::Html => $this->sanitizer->html($value),
|
||||
ValueKind::Code => $this->sanitizer->code($value),
|
||||
ValueKind::Url => $this->sanitizer->url($value),
|
||||
ValueKind::Text => $this->sanitizer->text($value),
|
||||
};
|
||||
|
||||
// Step 8 (S28): sanitizers strip characters ("<x>", "%41") and can thereby build a marker
|
||||
@@ -218,31 +213,14 @@ final class ValueCleaner
|
||||
return $clean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the kind override $kind is at least as strict as the routed kind (ruling S33a).
|
||||
* Strictness within the text family: code (raw for unfiltered_html) < html (kses) < text
|
||||
* (no markup). url (esc_url_raw) is a family of its own: it neither tightens nor is
|
||||
* tightened by the others, so only url → url passes.
|
||||
*/
|
||||
private static function tightens(string $kind, string $routed): bool
|
||||
{
|
||||
if ($kind === 'url' || $routed === 'url') {
|
||||
return $kind === $routed;
|
||||
}
|
||||
|
||||
return self::STRICTNESS[$kind] >= self::STRICTNESS[$routed];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizer for a string (rulings S3/S3a/S23/S24): the catalog kind of the path – or of the
|
||||
* sub-field for a composite path – decides. A string reached through a key below a path
|
||||
* with one scalar kind counts as unknown (S24). Unknown falls back to the key name (URL keys
|
||||
* at any depth), then to the last path segment (…Url), then content.innerContent → rich
|
||||
* text (S23: fields Divi defines only in JS), else text – never laxer than text.
|
||||
*
|
||||
* @return 'html'|'code'|'url'|'text'
|
||||
*/
|
||||
private function kind(string $module, string $path, ?string $subName, ?string $key): string
|
||||
private function kind(string $module, string $path, ?string $subName, ?string $key): ValueKind
|
||||
{
|
||||
// "unknown" also for a composite path whose sub-kinds do not name $subName.
|
||||
$kind = $this->catalog->pathKind($module, $path, $subName);
|
||||
@@ -251,26 +229,26 @@ final class ValueCleaner
|
||||
}
|
||||
|
||||
return match ($kind) {
|
||||
'richtext' => 'html',
|
||||
'code' => 'code',
|
||||
'url', 'image' => 'url',
|
||||
'richtext' => ValueKind::Html,
|
||||
'code' => ValueKind::Code,
|
||||
'url', 'image' => ValueKind::Url,
|
||||
'unknown' => $this->unknownKind($path, $key),
|
||||
default => 'text',
|
||||
default => ValueKind::Text,
|
||||
};
|
||||
}
|
||||
|
||||
/** @return 'html'|'url'|'text' */
|
||||
private function unknownKind(string $path, ?string $key): string
|
||||
/** Html, Url or Text – never Code. */
|
||||
private function unknownKind(string $path, ?string $key): ValueKind
|
||||
{
|
||||
if ($key !== null) {
|
||||
return preg_match(self::URL_KEY, $key) === 1 ? 'url' : 'text';
|
||||
return preg_match(self::URL_KEY, $key) === 1 ? ValueKind::Url : ValueKind::Text;
|
||||
}
|
||||
|
||||
$segments = explode('.', $path);
|
||||
if (preg_match(self::URL_SEGMENT, (string) end($segments)) === 1) {
|
||||
return 'url';
|
||||
return ValueKind::Url;
|
||||
}
|
||||
|
||||
return $path === 'content.innerContent' ? 'html' : 'text';
|
||||
return $path === 'content.innerContent' ? ValueKind::Html : ValueKind::Text;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user