that logs nothing). */ final class StoredNodeGuardTest extends TestCase { private const PROBE_HTML = ''; private const PROBE_SCRIPT = ''; private static function guard(bool $unfiltered = false): StoredNodeGuard { $references = new VariableRef( DiviFixtures::designSystem(), DynamicContentMap::fromOptions(DiviFixtures::dynamicContentRegistry()) ); $sanitizer = ComposeFixture::sanitizer($unfiltered); return new StoredNodeGuard(new ValueCleaner(DiviFixtures::catalog(), $references, $sanitizer), $references, $sanitizer); } private static function block(string $markup): Block { return (new Parser())->parse($markup)->children()[0]; } /** * A void block the way WordPress/Divi serialize it: quotes inside strings (so inside a * $variable(…)$ payload) become " – ruling P2. * * @param array $attrs */ private static function node(string $name, array $attrs): string { return ''; } /** @param array $settings */ private static function ref(string $name, array $settings): string { return VariableRef::MARKER . json_encode( ['type' => 'content', 'value' => ['name' => $name, 'settings' => (object) $settings]], JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR ) . VariableRef::END; } /** @return array */ private static function desktop(mixed $value): array { return ['desktop' => ['value' => $value]]; } private static function text(string $html): string { return self::node('divi/text', ['content' => ['innerContent' => self::desktop($html)]]); } public function testBuilderContentWithInstancesAndLinksPasses(): void { $index = NodeIndex::build((new Parser())->parse(DiviFixtures::markup('stage3/page--globale-instanzen.html'))); $link = self::block(self::node('divi/button', ['button' => ['innerContent' => self::desktop([ 'text' => 'Kontakt', 'linkUrl' => self::ref('post_link_url_page', ['post_id' => '839']), ])]])); $title = self::block(self::text(self::ref('post_title', ['before' => 'Titel:', 'after' => '']))); self::assertFalse($link->isOpaque(), 'the link must really be parsed (P2)'); self::assertFalse($title->isOpaque()); self::assertSame([], self::guard()->problems($index->find('s0'))); self::assertSame([], self::guard()->problems($index->find('g0'))); self::assertSame([], self::guard()->problems($link)); self::assertSame([], self::guard()->problems($title)); self::assertSame([], self::guard()->problems(self::block(self::text('

Tom & Jerry

')))); } /** @return array */ public static function libraryEntries(): array { return [ 'global section' => ['stage3/library-global--globaler-abschnitt.html'], 'global module' => ['stage3/library-global--globales-modul.html'], 'full page layout' => ['stage3/library--layout-ganze-seite.html'], 'section' => ['stage3/library--library-abschnitt.html'], 'module' => ['stage3/library--library-modul.html'], 'row' => ['stage3/library--library-zeile.html'], ]; } /** @dataProvider libraryEntries */ public function testRealLibraryEntriesPass(string $fixture): void { $document = (new Parser())->parse(DiviFixtures::markup($fixture)); foreach ($document->children() as $root) { self::assertSame([], self::guard()->problems($root)); } } /** @return array Markup, part of the problem, parsed opaque. */ public static function refused(): array { $section = static fn (string $inner): string => '' . $inner . ''; return [ // P3: builder-only paths (S7/S26). 'custom css' => [ self::node('divi/text', ['css' => self::desktop(['freeForm' => 'body{display:none}'])]), '"css"', ], 'custom css of a sub-element' => [ self::node('divi/blurb', ['title' => ['css' => self::desktop(['freeForm' => 'a{}'])]]), 'title.css', ], 'html attributes' => [ self::node('divi/text', ['module' => ['decoration' => ['attributes' => self::desktop([ 'attributes' => [['name' => 'data-probe', 'value' => '1']], ])]]]), 'module.decoration.attributes', ], 'own html before the module' => [ self::node('divi/text', ['module' => ['advanced' => ['html' => self::desktop([ 'htmlBefore' => self::PROBE_SCRIPT, ])]]]), 'module.advanced.html', ], 'own html below a state value' => [ self::node('divi/text', ['module' => ['advanced' => self::desktop([ 'html' => ['htmlBefore' => self::PROBE_SCRIPT], ])]]), 'module.advanced.html', ], 'interactions (P16)' => [ self::node('divi/text', ['module' => ['decoration' => ['interactions' => self::desktop([ 'interactions' => [[ 'trigger' => 'click', 'effect' => 'addAttribute', 'attributeName' => 'data-probe', 'attributeValue' => '1', ]], ])]]]), 'module.decoration.interactions', ], // Code module (E6). 'code module' => [ self::node('divi/code', ['content' => ['innerContent' => self::desktop(self::PROBE_SCRIPT)]]), 'Code-Modul', ], 'code module deep inside a section' => [$section(''), 'Code-Modul'], // P3: non-Divi blocks and raw text parts. 'core html block' => [ '' . self::PROBE_HTML . '', 'kein Divi-Block', ], 'core block inside a section' => [ $section('

x

'), 'kein Divi-Block', ], 'raw text between blocks' => [$section(self::PROBE_HTML), 'Freitext'], 'divi 4 shortcode between blocks' => [$section('[et_pb_code]probe[/et_pb_code]'), 'Freitext'], // P3: opaque blocks. 'undecodable attributes' => ['', 'nicht lesbar', true], 'global module reference on a plain block' => [ self::node('divi/text', ['globalModule' => '5']), 'nicht lesbar', true, ], 'instance without block name' => [ self::node('divi/global-layout', ['globalModule' => '5']), 'nicht lesbar', true, ], 'instance with local attributes that are no object' => [ self::node('divi/global-layout', ['globalModule' => '5', 'blockName' => 'divi/text', 'localAttrs' => 'x']), 'nicht lesbar', true, ], 'instance of a code module' => [ self::node('divi/global-layout', ['globalModule' => '5', 'blockName' => 'divi/code']), 'Code-Modul', true, ], 'css in the local attributes of an instance' => [ self::node('divi/global-layout', [ 'globalModule' => '5', 'blockName' => 'divi/text', 'localAttrs' => ['css' => self::desktop(['freeForm' => 'a{}'])], ]), '"css"', true, ], 'rich text in the local attributes of an instance' => [ self::node('divi/global-layout', [ 'globalModule' => '5', 'blockName' => 'divi/text', 'localAttrs' => ['content' => ['innerContent' => self::desktop(self::PROBE_HTML)]], ]), 'content.innerContent', true, ], 'script in the snapshot of an instance' => [ '' . '' . self::text(self::PROBE_SCRIPT) . '', 'content.innerContent', true, ], // P3: rich text that is not kses-stable. 'probe element in rich text' => [self::text('

Hallo

' . self::PROBE_HTML), 'content.innerContent'], 'script in rich text' => [self::text(self::PROBE_SCRIPT), 'content.innerContent'], 'script in a text field' => [ self::node('divi/heading', ['title' => ['innerContent' => self::desktop(self::PROBE_SCRIPT)]]), 'title.innerContent', ], 'script url' => [ self::node('divi/button', ['button' => ['innerContent' => self::desktop([ 'text' => 'Kontakt', 'linkUrl' => 'javascript:void(0)', ])]]), 'button.innerContent', ], 'control character' => [self::text('

a' . chr(1) . 'b

'), 'Steuerzeichen'], // Stored $variable references: the same check as client references (VariableRef). 'script in before' => [ self::text(self::ref('post_title', ['before' => self::PROBE_SCRIPT, 'after' => ''])), 'Divi-Verweis mit Einstellungen', ], 'probe html in after' => [ self::text(self::ref('post_title', ['before' => '', 'after' => self::PROBE_HTML])), 'Divi-Verweis mit Einstellungen', ], 'protected meta' => [self::text(self::ref('custom_meta__edit_lock', [])), 'custom_meta__edit_lock'], 'enable_html' => [ self::text(self::ref('post_meta_key', [ 'before' => '', 'select_meta_key' => 'custom_meta_telefon', 'enable_html' => 'on', ])), 'enable_html', ], 'reference embedded in text' => [ self::text('

Hallo ' . self::ref('post_title', []) . '

'), 'allein im Wert', ], 'content reference as a link' => [ self::node('divi/button', ['button' => ['innerContent' => self::desktop([ 'linkUrl' => self::ref('post_title', ['before' => self::PROBE_HTML]), ])]]), 'button.innerContent', ], ]; } /** @dataProvider refused */ public function testRefusesWhatTheUserCouldNotWriteForUsersWithoutUnfilteredHtml( string $markup, string $problem, bool $opaque = false ): void { $block = self::block($markup); self::assertSame($opaque, $block->isOpaque(), 'the row must parse as intended (P2)'); $problems = self::guard()->problems($block); self::assertNotSame([], $problems); self::assertStringContainsString($problem, implode(' | ', $problems)); self::assertSame([], self::guard(true)->problems($block), 'unfiltered_html may carry it'); } public function testNamesEveryProblemOnceAndLeavesTheNodeUntouched(): void { $markup = '' . self::node('divi/code', []) . self::node('divi/code', []) . self::text(self::PROBE_SCRIPT) . ''; $block = self::block($markup); $problems = self::guard()->problems($block); self::assertCount(2, $problems); self::assertSame($markup, Serializer::block($block)); } }