*/ public static function valid(): array { return [ 'reordered keys' => [ '$variable({"value":{"settings":{},"name":"gcid-a1b2"},"type":"color"})$', '$variable({"type":"color","value":{"name":"gcid-a1b2","settings":{}}})$', ], 'empty list settings, no settings' => [ '$variable({"type":"color","value":{"name":"gcid-primary-color","settings":[]}})$', '$variable({"type":"color","value":{"name":"gcid-primary-color","settings":{}}})$', ], 'missing settings' => [ '$variable({"type":"content","value":{"name":"gvid-r1"}})$', '$variable({"type":"content","value":{"name":"gvid-r1","settings":{}}})$', ], 'color settings numeric' => [ '$variable({"type":"color","value":{"name":"gcid-a1b2","settings":{"opacity":"50","hue":-10,"lightness":1.5}}})$', '$variable({"type":"color","value":{"name":"gcid-a1b2","settings":{"opacity":"50","hue":-10,"lightness":1.5}}})$', ], 'post date with format and html before' => [ '$variable({"type":"content","value":{"name":"post_date","settings":{"before":"Am ","after":"","date_format":"custom","custom_date_format":"d.m.Y"}}})$', '$variable({"type":"content","value":{"name":"post_date","settings":{"before":"Am ","after":"","date_format":"custom","custom_date_format":"d.m.Y"}}})$', ], 'custom meta' => [ '$variable({"type":"content","value":{"name":"custom_meta_telefon-mobil","settings":{"before":"","after":""}}})$', '$variable({"type":"content","value":{"name":"custom_meta_telefon-mobil","settings":{"before":"","after":""}}})$', ], 'theme font' => [ '$variable({"type":"content","value":{"name":"--et_global_body_font","settings":{}}})$', '$variable({"type":"content","value":{"name":"--et_global_body_font","settings":{}}})$', ], ]; } /** @dataProvider valid */ public function testReserializesValidReferences(string $input, string $expected): void { self::assertSame($expected, self::ref()->canonical($input, ComposeFixture::sanitizer())); } /** * Hand-crafted attacks (preflight §4 "Malicious $variable tests"). * * @return array Value => part of the German error. */ public static function malicious(): array { $content = static fn (string $name, string $settings): string => '$variable({"type":"content","value":{"name":"' . $name . '","settings":' . $settings . '}})$'; return [ 'script in before' => [$content('post_title', '{"before":"","after":""}'), 'before'], 'script in after' => [$content('post_title', '{"before":"","after":""}'), 'after'], 'escaped script in before' => [ $content('post_title', '{"before":"","after":""}'), 'before', ], 'tags in date format' => [$content('post_date', '{"date_format":"d"}'), 'date_format'], 'type evil' => ['$variable({"type":"evil","value":{"name":"gcid-a1b2","settings":{}}})$', 'Typ'], 'unknown gcid' => ['$variable({"type":"color","value":{"name":"gcid-nope","settings":{}}})$', 'gcid-nope'], 'unknown gvid' => [$content('gvid-nope', '{}'), 'gvid-nope'], 'content gvid as color' => ['$variable({"type":"color","value":{"name":"gvid-s1","settings":{}}})$', 'gvid-s1'], 'color gcid as content' => [$content('gcid-a1b2', '{}'), 'gcid-a1b2'], 'name with paren' => ['$variable({"type":"color","value":{"name":"gcid-a1b2)","settings":{}}})$', 'Name'], 'name with style breakout' => [ '$variable({"type":"color","value":{"name":"gcid-a1b2;}","settings":{}}})$', 'Name', ], 'unknown settings key' => [$content('post_title', '{"before":"","foo":"1"}'), 'foo'], 'enable_html' => [$content('custom_meta_telefon', '{"before":"","after":"","enable_html":"on"}'), 'enable_html'], 'color setting not numeric' => [ '$variable({"type":"color","value":{"name":"gcid-a1b2","settings":{"opacity":"1;}"}}})$', 'opacity', ], 'protected meta key' => [$content('custom_meta__edit_lock', '{}'), 'custom_meta__edit_lock'], 'unknown dynamic content' => [$content('post_meta_key', '{}'), 'post_meta_key'], 'extra top-level key' => ['$variable({"type":"color","value":{"name":"gcid-a1b2","settings":{}},"x":1})$', 'x'], 'extra value key' => ['$variable({"type":"color","value":{"name":"gcid-a1b2","settings":{},"x":1}})$', 'x'], 'greedy smuggling' => ['$variable({"a":"})$"})$', 'allein im Wert'], 'nested reference in before' => [ $content('post_title', '{"before":"$variable(x)$","after":""}'), 'allein im Wert', ], 'broken json' => ['$variable(kaputt)$', 'JSON'], 'trailing text' => [$content('post_title', '{}') . ' ', 'allein im Wert'], 'embedded in text' => ['

Hi ' . $content('post_title', '{"before":"","after":""}') . '

', 'allein im Wert'], ]; } /** @dataProvider malicious */ public function testRejectsMaliciousReferences(string $input, string $message): void { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage($message); self::ref()->canonical($input, ComposeFixture::sanitizer()); } public function testUrlFieldsTakeContentReferencesWithoutBeforeOrAfter(): void { $ref = self::ref(); $link = '$variable({"type":"content","value":{"name":"post_link_url","settings":{}}})$'; self::assertSame($link, $ref->canonical($link, ComposeFixture::sanitizer(), true)); foreach ( [ '$variable({"type":"content","value":{"name":"post_link_url","settings":{"before":"javascript:alert(1)//","after":""}}})$', '$variable({"type":"color","value":{"name":"gcid-a1b2","settings":{}}})$', ] as $value ) { try { $ref->canonical($value, ComposeFixture::sanitizer(), true); self::fail('Accepted in a URL field: ' . $value); } catch (InvalidArgumentException $e) { self::assertStringContainsString('URL', $e->getMessage()); } } } public function testDetectsReferences(): void { self::assertTrue(VariableRef::contains('a $variable( b')); self::assertFalse(VariableRef::contains('Preis: 5 $')); } }