diff --git a/docs/guides/divi-attrs.md b/docs/guides/divi-attrs.md index 7669eba..ffe0f85 100644 --- a/docs/guides/divi-attrs.md +++ b/docs/guides/divi-attrs.md @@ -38,8 +38,9 @@ zusammengeführt): z. B. bei `module.decoration.sizing` einer Spalte `flexType` Unbekannte Pfade → Fehler `invalid_attr_path` mit Vorschlägen unter `fields`. Texte werden bereinigt (HTML nur, wo Divi Rich-Text erwartet; Links ohne `javascript:`). `$variable`-Verweise werden geparst, -geprüft und vom Plugin neu erzeugt (siehe `divi-placeholders`). Eigenes HTML, CSS, HTML-Attribute und -Interaktionen (`…decoration.attributes`, `…decoration.interactions`, `…advanced.html`, `…css…`) nur mit dem Recht `unfiltered_html`, sonst „nur im +geprüft und vom Plugin neu erzeugt (siehe `divi-placeholders`). Eigenes HTML, CSS, HTML-Attribute, +Interaktionen und die Lottie-Quelle (`…decoration.attributes`, `…decoration.interactions`, `…advanced.html`, `…css…`, +`lottie.innerContent`) nur mit dem Recht `unfiltered_html`, sonst „nur im Divi-Builder“. Unberührte Teile der Seite bleiben byte-identisch; Ausnahme: Divi selbst normalisiert Seiten mit eigenen HTML-/Attribut-Feldern beim Speichern – nur wenn die Seite noch nicht in Divis eigener gespeicherter Form vorliegt (z. B. importierter oder direkt in die Datenbank geschriebener Inhalt); über den Divi-Builder oder LIT MCP gespeicherte Seiten betrifft das praktisch nie (Warnung in der Antwort, neue `tree_rev` gilt). diff --git a/docs/guides/divi-shortform.md b/docs/guides/divi-shortform.md index 694e5e2..fabee59 100644 --- a/docs/guides/divi-shortform.md +++ b/docs/guides/divi-shortform.md @@ -106,8 +106,8 @@ Bedarf die Vorlage im Builder neu wählen). Alle Werte werden vor dem Speichern bereinigt: Texte ohne HTML, Rich-Text ohne `unfiltered_html` nur mit dem HTML, das WordPress in Beiträgen erlaubt, Links ohne `javascript:`. -`$variable`-Verweise werden geparst, geprüft und vom Plugin neu erzeugt. Eigenes HTML, CSS, HTML-Attribute und Interaktionen eines Moduls (`…decoration.attributes`, -`…decoration.interactions`, `…advanced.html`, `…css…`) ändert nur, wer das Recht `unfiltered_html` hat – sonst „nur im Divi-Builder“. Unberührte Teile der +`$variable`-Verweise werden geparst, geprüft und vom Plugin neu erzeugt. Eigenes HTML, CSS, HTML-Attribute, Interaktionen und die Lottie-Quelle eines Moduls (`…decoration.attributes`, +`…decoration.interactions`, `…advanced.html`, `…css…`, `lottie.innerContent`) ändert nur, wer das Recht `unfiltered_html` hat – sonst „nur im Divi-Builder“. Unberührte Teile der Seite bleiben byte-identisch; Ausnahme: Divi selbst normalisiert Seiten mit eigenen HTML-/Attribut-Feldern beim Speichern – nur wenn die Seite noch nicht in Divis eigener gespeicherter Form vorliegt (z. B. importierter oder direkt in die Datenbank geschriebener Inhalt); über den Divi-Builder oder LIT MCP gespeicherte Seiten betrifft das praktisch nie (dann Warnung, dass der Inhalt beim Speichern verändert wurde – mit der neuen `tree_rev` weiterarbeiten). diff --git a/src/Integrations/Divi/Compose/ValueCleaner.php b/src/Integrations/Divi/Compose/ValueCleaner.php index d828329..531d3f1 100644 --- a/src/Integrations/Divi/Compose/ValueCleaner.php +++ b/src/Integrations/Divi/Compose/ValueCleaner.php @@ -56,10 +56,12 @@ final class ValueCleaner * path shape at any depth: module.advanced.html(.htmlBefore), module.decoration.attributes, * any free-form custom CSS path of any module (css, css.*, title.css.*, css.advanced.customCss), * and interactions (ruling P16: *.decoration.interactions – Divi's frontend script applies the - * "Attribute" effect with setAttribute, e.g. an onclick handler, and sets cookies/presets). + * "Attribute" effect with setAttribute, e.g. an onclick handler, and sets cookies/presets), + * and the Lottie source (ruling P19: lottie.innerContent – src becomes data-lottie-src, and the + * lottie-web build runs animation expressions with eval). */ - private const BUILDER_ONLY - = '/(^|\.)(decoration\.attributes|decoration\.interactions|advanced\.html|advanced\.customCss|css)(\.|$)/'; + private const BUILDER_ONLY = '/(^|\.)(decoration\.attributes|decoration\.interactions|advanced\.html' + . '|advanced\.customCss|css|lottie\.innerContent)(\.|$)/'; public function __construct( private ModuleCatalog $catalog, @@ -137,8 +139,8 @@ final class ValueCleaner { if (preg_match(self::BUILDER_ONLY, $path) === 1 && ! $this->sanitizer->allowsUnfilteredHtml()) { throw new InvalidArgumentException(sprintf( - '"%s" (eigenes HTML, CSS, HTML-Attribute oder Interaktionen) ist nur im Divi-Builder änderbar ' - . '(oder mit dem Recht unfiltered_html).', + '"%s" (eigenes HTML, CSS, HTML-Attribute, Interaktionen oder Lottie-Quelle) ist nur im ' + . 'Divi-Builder änderbar (oder mit dem Recht unfiltered_html).', $path )); } diff --git a/tests/Unit/Integrations/Divi/Compose/AttrWriterTest.php b/tests/Unit/Integrations/Divi/Compose/AttrWriterTest.php index 9c5105d..a8a5adf 100644 --- a/tests/Unit/Integrations/Divi/Compose/AttrWriterTest.php +++ b/tests/Unit/Integrations/Divi/Compose/AttrWriterTest.php @@ -6,6 +6,8 @@ namespace LIT\MCP\Tests\Unit\Integrations\Divi\Compose; use InvalidArgumentException; use LIT\MCP\Integrations\Divi\Compose\AttrWriter; +use LIT\MCP\Integrations\Divi\Compose\ValueCleaner; +use LIT\MCP\Integrations\Divi\Schema\ModuleCatalog; use LIT\MCP\Tests\Support\ComposeFixture; use LIT\MCP\Tests\Support\DiviFixtures; use PHPUnit\Framework\TestCase; @@ -405,4 +407,52 @@ final class AttrWriterTest extends TestCase self::assertSame([], $adminRemoval['errors']); self::assertArrayNotHasKey('interactions', $adminRemoval['attrs']['module']['decoration'] ?? []); } + + /** + * The Lottie module of Divi 5.2.1 (excerpt of lottie/module.json: the animation source group) – + * the sample metadata has no Lottie module. + */ + private static function lottieWriter(bool $unfiltered): AttrWriter + { + $catalog = new ModuleCatalog([[ + 'name' => 'divi/lottie', + 'title' => 'Lottie', + 'category' => 'module', + 'attributes' => ['lottie' => ['type' => 'object', 'settings' => ['innerContent' => [ + 'groupType' => 'group-item', + 'item' => [ + 'attrName' => 'lottie.innerContent', + 'component' => ['type' => 'field', 'name' => 'divi/lottie'], + ], + ]]]], + ]]); + $cleaner = new ValueCleaner($catalog, ComposeFixture::references(), ComposeFixture::sanitizer($unfiltered)); + + return new AttrWriter($catalog, ComposeFixture::placeholders(), $cleaner); + } + + /** Ruling P19: the Lottie source only with unfiltered_html – set and removal. */ + public function testLottieSourceNeedsUnfilteredHtml(): void + { + $changes = ['lottie.innerContent' => ['desktop' => ['src' => 'https://probe.example/anim.json']]]; + $remove = ['lottie.innerContent' => null]; + $key = 'attrs.lottie.innerContent'; + + $editor = self::lottieWriter(false)->apply('divi/lottie', [], $changes, 'attrs'); + self::assertSame([], $editor['attrs']); + self::assertStringContainsString('nur im Divi-Builder', $editor['errors'][$key]); + + $removal = self::lottieWriter(false)->apply('divi/lottie', [], $remove, 'attrs'); + self::assertStringContainsString('nur im Divi-Builder', $removal['errors'][$key]); + + $admin = self::lottieWriter(true)->apply('divi/lottie', [], $changes, 'attrs'); + self::assertSame([], $admin['errors']); + self::assertSame( + 'https://probe.example/anim.json', + $admin['attrs']['lottie']['innerContent']['desktop']['value']['src'] + ); + $adminRemoval = self::lottieWriter(true)->apply('divi/lottie', $admin['attrs'], $remove, 'attrs'); + self::assertSame([], $adminRemoval['errors']); + self::assertArrayNotHasKey('lottie', $adminRemoval['attrs']); + } } diff --git a/tests/Unit/Integrations/Divi/Compose/StoredNodeGuardTest.php b/tests/Unit/Integrations/Divi/Compose/StoredNodeGuardTest.php index c3867d2..237572b 100644 --- a/tests/Unit/Integrations/Divi/Compose/StoredNodeGuardTest.php +++ b/tests/Unit/Integrations/Divi/Compose/StoredNodeGuardTest.php @@ -164,6 +164,13 @@ final class StoredNodeGuardTest extends TestCase ])]]]), 'module.decoration.interactions', ], + 'lottie source (P19)' => [ + self::node('divi/lottie', ['lottie' => ['innerContent' => self::desktop([ + 'src' => 'https://probe.example/anim.json', + 'trigger' => 'onLoad', + ])]]), + 'lottie.innerContent', + ], // Code module (E6). 'code module' => [ self::node('divi/code', ['content' => ['innerContent' => self::desktop(self::PROBE_SCRIPT)]]), diff --git a/tests/Unit/Integrations/Divi/Compose/ValueCleanerTest.php b/tests/Unit/Integrations/Divi/Compose/ValueCleanerTest.php index ab21058..7543687 100644 --- a/tests/Unit/Integrations/Divi/Compose/ValueCleanerTest.php +++ b/tests/Unit/Integrations/Divi/Compose/ValueCleanerTest.php @@ -232,4 +232,25 @@ final class ValueCleanerTest extends TestCase self::assertSame('x', ComposeFixture::cleaner()->clean('divi/text', 'module.decoration.interactionsish', 'x')); self::assertSame('x', ComposeFixture::cleaner()->clean('divi/text', 'module.advanced.interactions', 'x')); } + + /** Ruling P19: the Lottie source (lottie-web runs expressions with eval) is builder-only, at every depth. */ + public function testLottieSourceNeedsUnfilteredHtml(): void + { + $value = ['src' => 'https://probe.example/anim.json', 'loop' => 'on']; + $paths = ['lottie.innerContent', 'lottie.innerContent.desktop', 'lottie.innerContent.desktop.value.src']; + foreach ($paths as $path) { + try { + ComposeFixture::cleaner()->clean('divi/lottie', $path, $value); + self::fail('Accepted for an editor: ' . $path); + } catch (InvalidArgumentException $e) { + self::assertStringContainsString('nur im Divi-Builder', $e->getMessage()); + } + + self::assertSame($value, ComposeFixture::cleaner(true)->clean('divi/lottie', $path, $value)); + } + + // The size of the animation and look-alike paths stay open. + self::assertSame('x', ComposeFixture::cleaner()->clean('divi/lottie', 'lottie.advanced.width', 'x')); + self::assertSame('x', ComposeFixture::cleaner()->clean('divi/lottie', 'lottie.innerContentish', 'x')); + } }