feat: Divi save pipeline – tree_rev lock, kses-safe save, revision, flags, cache, ToolResult

DiviSaver/DiviWriter/DiviComposition/DiviChange, WpValueSanitizer (S4),
WpMediaResolver, DiviFlags (S11), DynamicContentMap from Divi's live
registry (S22), S31 wiring, S35 control strip at apply() entry.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-26 14:56:47 +02:00
co-authored by Claude Opus 5.5
parent ea1fd607b2
commit 7762b76305
20 changed files with 1716 additions and 2 deletions
@@ -58,6 +58,8 @@ final class AttrWriter
*/
public function apply(string $module, array $attrs, array $changes, string $field): array
{
// Ruling S35: controls go before placeholder resolution ("{\0{post:title}}" is a placeholder).
$changes = VariableRef::withoutControlsDeep($changes);
$errors = [];
$invalid = [];
foreach ($changes as $path => $map) {
@@ -48,6 +48,8 @@ final class ShortformWriter
*/
public function apply(string $type, array $attrs, array $values, string $field): array
{
// Ruling S35: controls go before placeholder resolution and trim ("{\0{post:title}}").
$values = VariableRef::withoutControlsDeep($values);
$fields = $this->modules === null ? ShortformMap::fields($type) : ($this->modules[$type] ?? []);
$errors = [];
foreach ($values as $name => $value) {
+17 -1
View File
@@ -146,6 +146,21 @@ final class VariableRef
return (string) preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $value);
}
/**
* withoutControls() on every string inside $value (keys stay: odd keys are rejected). Ruling
* S35: AttrWriter::apply() and ShortformWriter::apply() run it once on their raw input, before
* placeholder resolution and trim – so "{\0{post:title}}" is the placeholder, checked as one,
* and never literal text that slipped past the placeholder check.
*/
public static function withoutControlsDeep(mixed $value): mixed
{
if (is_string($value)) {
return self::withoutControls($value);
}
return is_array($value) ? array_map([self::class, 'withoutControlsDeep'], $value) : $value;
}
/** @param array<string, mixed>|stdClass $settings */
public static function serialize(string $type, string $name, array|stdClass $settings): string
{
@@ -383,7 +398,8 @@ final class VariableRef
}
// Ruling S29: meta key settings (post_meta_key's meta_key/select_meta_key, loop_meta_key …)
// name a meta key – never a protected one.
// name a meta key – never a protected one. Matched by the key SUFFIX "meta_key" (S29/S30):
// re-check Divi's dynamic content options for new meta key settings on every Divi version bump.
if (str_ends_with($key, 'meta_key') && $clean !== '' && ! self::isMetaKey((string) $clean)) {
throw new InvalidArgumentException(sprintf(
'Einstellung "%s" im Divi-Verweis: ungültiger Meta-Schlüssel (Buchstaben, Ziffern, "_" und "-", '