fix: Task 1 review round 1 - escaped )$ in reference settings, allow map, routing rulings
I-1: reject )$ in every decoded string setting and in the re-serialized JSON. S21: string settings cleaned and re-serialized. S22: injectable dynamic-content allow map (name => setting => field type), loop_* only via the map. S23: unknown content.innerContent is rich text. S24: keys below scalar kinds are unknown, ...Url path segments are URLs. S25: isVariable removed. M-1: real \u escapes in tests, malicious suite as editor, fixture html() raw for unfiltered_html. M-2: S7 gate at any depth plus css.advanced.customCss. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ namespace LIT\MCP\Tests\Unit\Integrations\Divi\Compose;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use LIT\MCP\Integrations\Divi\Compose\AttrWriter;
|
||||
use LIT\MCP\Tests\Unit\Integrations\Divi\Schema\ModuleCatalogTest;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class AttrWriterTest extends TestCase
|
||||
@@ -147,7 +148,8 @@ final class AttrWriterTest extends TestCase
|
||||
}
|
||||
|
||||
/**
|
||||
* Ruling S6: each malicious reference fails and nothing is stored.
|
||||
* Ruling S6: each malicious reference fails and nothing is stored (as an editor without
|
||||
* unfiltered_html).
|
||||
*
|
||||
* @return array<string, array{string, string, array<string, mixed>, array<string, mixed>}>
|
||||
*/
|
||||
@@ -196,12 +198,25 @@ final class AttrWriterTest extends TestCase
|
||||
*/
|
||||
public function testRejectsMaliciousReferencesAndStoresNothing(string $module, string $path, array $map, array $attrs): void
|
||||
{
|
||||
$result = ComposeFixture::attrWriter(true)->apply($module, $attrs, [$path => $map], 'attrs');
|
||||
$result = ComposeFixture::attrWriter()->apply($module, $attrs, [$path => $map], 'attrs');
|
||||
|
||||
self::assertArrayHasKey('attrs.' . $path, $result['errors']);
|
||||
self::assertSame($attrs, $result['attrs']);
|
||||
}
|
||||
|
||||
/** Ruling S21: an editor's script in before/after is cleaned out, the reference re-serialized. */
|
||||
public function testReferenceSettingsAreCleanedForEditors(): void
|
||||
{
|
||||
$sent = '$variable({"type":"content","value":{"name":"post_title","settings":{"after":"","before":"<script>alert(1)</script>Titel: "}}})$';
|
||||
$result = ComposeFixture::attrWriter()->apply('divi/text', self::TEXT, ['content.innerContent' => ['desktop' => $sent]], 'attrs');
|
||||
|
||||
self::assertSame([], $result['errors']);
|
||||
self::assertSame(
|
||||
'$variable({"type":"content","value":{"name":"post_title","settings":{"after":"","before":"Titel: "}}})$',
|
||||
$result['attrs']['content']['innerContent']['desktop']['value']
|
||||
);
|
||||
}
|
||||
|
||||
public function testEmbeddedReferenceNamesTheRule(): void
|
||||
{
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
@@ -231,11 +246,46 @@ final class AttrWriterTest extends TestCase
|
||||
self::assertSame('section', $admin['attrs']['module']['advanced']['html']['desktop']['value']['elementType']);
|
||||
self::assertSame('data-x', $admin['attrs']['module']['decoration']['attributes']['desktop']['value']['attributes'][0]['name']);
|
||||
|
||||
// Deeper paths and the canvas portal's custom CSS are gated too (fix round 1, M-2).
|
||||
foreach (['module.advanced.html.htmlBefore', 'module.decoration.attributes.desktop', 'css.advanced.customCss'] as $path) {
|
||||
try {
|
||||
ComposeFixture::attrWriter()->cleanValue('divi/canvas-portal', $path, 'x');
|
||||
self::fail('Accepted for an editor: ' . $path);
|
||||
} catch (InvalidArgumentException $e) {
|
||||
self::assertStringContainsString('nur im Divi-Builder', $e->getMessage());
|
||||
}
|
||||
}
|
||||
self::assertSame('x', ComposeFixture::attrWriter(true)->cleanValue('divi/canvas-portal', 'css.advanced.customCss', 'x'));
|
||||
self::assertSame('x', ComposeFixture::attrWriter()->cleanValue('divi/text', 'module.advanced.htmlish', 'x'));
|
||||
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('nur im Divi-Builder');
|
||||
ComposeFixture::attrWriter()->cleanValue('divi/button', 'module.decoration.attributes', ['attributes' => []]);
|
||||
}
|
||||
|
||||
/** Ruling S23: content.innerContent of unknown kind (defined only in Divi's JS) is rich text. */
|
||||
public function testUnknownContentInnerContentIsRichText(): void
|
||||
{
|
||||
self::assertSame('unknown', ModuleCatalogTest::sampleCatalog()->pathKind('divi/blurb', 'content.innerContent'));
|
||||
self::assertSame('<p>Hallo</p>', ComposeFixture::attrWriter()->cleanValue('divi/blurb', 'content.innerContent', '<p>Hallo</p><script>x()</script>'));
|
||||
// Other unknown innerContent stays text.
|
||||
self::assertSame('Los', ComposeFixture::attrWriter()->cleanValue('divi/button', 'button.innerContent', '<b>Los</b>'));
|
||||
}
|
||||
|
||||
/** Ruling S24: keys below a scalar-kind path are unknown; a …Url path segment means URL. */
|
||||
public function testKeysBelowScalarKindsAndUrlPathSegments(): void
|
||||
{
|
||||
$writer = ComposeFixture::attrWriter();
|
||||
|
||||
self::assertSame(
|
||||
['url' => '', 'text' => 'a'],
|
||||
$writer->cleanValue('divi/heading', 'title.innerContent', ['url' => 'javascript:x', 'text' => '<b>a</b>'])
|
||||
);
|
||||
self::assertSame('', $writer->cleanValue('divi/contact-form', 'success.advanced.redirectUrl', 'javascript:alert(1)'));
|
||||
self::assertSame('', $writer->cleanValue('divi/text', 'module.advanced.imageUrl', 'javascript:alert(1)'));
|
||||
self::assertSame('Titel', $writer->cleanValue('divi/heading', 'title.innerContent', '<b>Titel</b>'));
|
||||
}
|
||||
|
||||
/** Ruling S13: keys inside value maps are plain identifiers. */
|
||||
public function testRejectsOddKeysInsideValues(): void
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user