fix: the Lottie source is builder-only for users without unfiltered_html (P19)
lottie.innerContent (src → data-lottie-src) makes the frontend load an animation whose expressions lottie-web runs with eval; it joins the S7 gate in ValueCleaner, which covers removals and StoredNodeGuard. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
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,
|
(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
|
geprüft und vom Plugin neu erzeugt (siehe `divi-placeholders`). Eigenes HTML, CSS, HTML-Attribute,
|
||||||
Interaktionen (`…decoration.attributes`, `…decoration.interactions`, `…advanced.html`, `…css…`) nur mit dem Recht `unfiltered_html`, sonst „nur im
|
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
|
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).
|
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).
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
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:`.
|
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`,
|
`$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…`) ändert nur, wer das Recht `unfiltered_html` hat – sonst „nur im Divi-Builder“. Unberührte Teile der
|
`…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
|
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`
|
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).
|
weiterarbeiten).
|
||||||
|
|||||||
@@ -56,10 +56,12 @@ final class ValueCleaner
|
|||||||
* path shape at any depth: module.advanced.html(.htmlBefore), module.decoration.attributes,
|
* 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),
|
* 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
|
* 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
|
private const BUILDER_ONLY = '/(^|\.)(decoration\.attributes|decoration\.interactions|advanced\.html'
|
||||||
= '/(^|\.)(decoration\.attributes|decoration\.interactions|advanced\.html|advanced\.customCss|css)(\.|$)/';
|
. '|advanced\.customCss|css|lottie\.innerContent)(\.|$)/';
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private ModuleCatalog $catalog,
|
private ModuleCatalog $catalog,
|
||||||
@@ -137,8 +139,8 @@ final class ValueCleaner
|
|||||||
{
|
{
|
||||||
if (preg_match(self::BUILDER_ONLY, $path) === 1 && ! $this->sanitizer->allowsUnfilteredHtml()) {
|
if (preg_match(self::BUILDER_ONLY, $path) === 1 && ! $this->sanitizer->allowsUnfilteredHtml()) {
|
||||||
throw new InvalidArgumentException(sprintf(
|
throw new InvalidArgumentException(sprintf(
|
||||||
'"%s" (eigenes HTML, CSS, HTML-Attribute oder Interaktionen) ist nur im Divi-Builder änderbar '
|
'"%s" (eigenes HTML, CSS, HTML-Attribute, Interaktionen oder Lottie-Quelle) ist nur im '
|
||||||
. '(oder mit dem Recht unfiltered_html).',
|
. 'Divi-Builder änderbar (oder mit dem Recht unfiltered_html).',
|
||||||
$path
|
$path
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ namespace LIT\MCP\Tests\Unit\Integrations\Divi\Compose;
|
|||||||
|
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
use LIT\MCP\Integrations\Divi\Compose\AttrWriter;
|
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\ComposeFixture;
|
||||||
use LIT\MCP\Tests\Support\DiviFixtures;
|
use LIT\MCP\Tests\Support\DiviFixtures;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
@@ -405,4 +407,52 @@ final class AttrWriterTest extends TestCase
|
|||||||
self::assertSame([], $adminRemoval['errors']);
|
self::assertSame([], $adminRemoval['errors']);
|
||||||
self::assertArrayNotHasKey('interactions', $adminRemoval['attrs']['module']['decoration'] ?? []);
|
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']);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -164,6 +164,13 @@ final class StoredNodeGuardTest extends TestCase
|
|||||||
])]]]),
|
])]]]),
|
||||||
'module.decoration.interactions',
|
'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 (E6).
|
||||||
'code module' => [
|
'code module' => [
|
||||||
self::node('divi/code', ['content' => ['innerContent' => self::desktop(self::PROBE_SCRIPT)]]),
|
self::node('divi/code', ['content' => ['innerContent' => self::desktop(self::PROBE_SCRIPT)]]),
|
||||||
|
|||||||
@@ -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.decoration.interactionsish', 'x'));
|
||||||
self::assertSame('x', ComposeFixture::cleaner()->clean('divi/text', 'module.advanced.interactions', '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'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user