test: shared Divi fixtures in tests/Support (T1)
- DiviFixtures: markup(), givenPost(), schema(), designSystem(), catalog(), dynamicContentRegistry(), maliciousRefs() - moved verbatim out of DiviPageLoaderTest, DesignSystemTest, ModuleCatalogTest, DynamicContentMapTest and VariableRefTest (its malicious() provider now returns maliciousRefs()). - ComposeFixture moves to tests/Support. - DiviWriterFixture: modules(), designReader(), composition(), a recording saver(), log() and writer(&$saved, &$failures, unfiltered, loader) for the five tests that wired the Divi write stack by hand. No test cases or assertions changed (1397 / 5834). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,8 @@ 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 LIT\MCP\Tests\Support\ComposeFixture;
|
||||
use LIT\MCP\Tests\Support\DiviFixtures;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class AttrWriterTest extends TestCase
|
||||
@@ -156,7 +157,7 @@ final class AttrWriterTest extends TestCase
|
||||
public static function malicious(): array
|
||||
{
|
||||
$cases = [];
|
||||
foreach (VariableRefTest::malicious() as $name => [$value]) {
|
||||
foreach (DiviFixtures::maliciousRefs() as $name => [$value]) {
|
||||
$cases[$name] = ['divi/text', 'content.innerContent', ['desktop' => $value], self::TEXT];
|
||||
}
|
||||
|
||||
@@ -317,7 +318,7 @@ final class AttrWriterTest extends TestCase
|
||||
/** 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('unknown', DiviFixtures::catalog()->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>'));
|
||||
|
||||
@@ -1,174 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Compose;
|
||||
|
||||
use LIT\MCP\Integrations\Divi\Compose\AttrWriter;
|
||||
use LIT\MCP\Integrations\Divi\Compose\Composer;
|
||||
use LIT\MCP\Integrations\Divi\Compose\MediaResolver;
|
||||
use LIT\MCP\Integrations\Divi\Compose\Placeholders;
|
||||
use LIT\MCP\Integrations\Divi\Compose\ShortformWriter;
|
||||
use LIT\MCP\Integrations\Divi\Compose\StructureRules;
|
||||
use LIT\MCP\Integrations\Divi\Compose\ValueSanitizer;
|
||||
use LIT\MCP\Integrations\Divi\Design\DesignSystem;
|
||||
use LIT\MCP\Tests\Unit\Integrations\Divi\Design\DesignSystemTest;
|
||||
use LIT\MCP\Tests\Unit\Integrations\Divi\Schema\ModuleCatalogTest;
|
||||
|
||||
/**
|
||||
* Compose classes wired with the real Divi 5.2.1 sample catalog, the sample design system and
|
||||
* predictable fakes for sanitizing and media (no WordPress). $unfiltered switches the fake
|
||||
* sanitizer between a user with and without unfiltered_html (ruling S7).
|
||||
*/
|
||||
final class ComposeFixture
|
||||
{
|
||||
/** A valid content reference as an attachment's alt text (media ids 44 and 45). */
|
||||
public const REFERENCE_ALT = '$variable({"type":"content","value":{"name":"post_title","settings":{}}})$';
|
||||
|
||||
public static function sanitizer(bool $unfiltered = false): ValueSanitizer
|
||||
{
|
||||
return new class ($unfiltered) implements ValueSanitizer {
|
||||
public function __construct(private bool $unfiltered)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Like sanitize_text_field (_sanitize_text_fields): tags only go when there is a "<"
|
||||
* (wp_strip_all_tags; PHP's strip_tags also drops NUL then), whitespace runs become one
|
||||
* space, trim, %XX octets go repeatedly. Without a "<", NUL and other control
|
||||
* characters stay – as in WordPress (ruling S32: AttrWriter strips them).
|
||||
*/
|
||||
public function text(string $value): string
|
||||
{
|
||||
if (str_contains($value, '<')) {
|
||||
$value = strip_tags((string) preg_replace('@<(script|style)[^>]*?>.*?</\\1>@si', '', $value));
|
||||
}
|
||||
|
||||
$value = trim((string) preg_replace('/[\r\n\t ]+/', ' ', $value));
|
||||
$found = false;
|
||||
while (preg_match('/%[a-f0-9]{2}/i', $value, $match) === 1) {
|
||||
$value = str_replace($match[0], '', $value);
|
||||
$found = true;
|
||||
}
|
||||
|
||||
return $found ? trim((string) preg_replace('/ +/', ' ', $value)) : $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Like WpValueSanitizer (ruling S4): raw for users with unfiltered_html; otherwise like
|
||||
* wp_kses_post: NUL bytes, <script> blocks and tags outside a small allow list go.
|
||||
*/
|
||||
public function html(string $value): string
|
||||
{
|
||||
if ($this->unfiltered) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
$value = (string) preg_replace('#<script\b[^>]*>.*?</script>#is', '', str_replace(chr(0), '', $value));
|
||||
|
||||
return (string) preg_replace(
|
||||
'#</?(?!(?:p|b|i|em|strong|a|br|span|h[1-6]|ul|ol|li)\b)[a-z][^>]*>#i',
|
||||
'',
|
||||
$value
|
||||
);
|
||||
}
|
||||
|
||||
public function code(string $value): string
|
||||
{
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Like esc_url_raw: spaces become %20, characters outside its allow list (tags, quotes,
|
||||
* braces, NUL) go, %0d/%0a are removed repeatedly, javascript: gives ''.
|
||||
*/
|
||||
public function url(string $value): string
|
||||
{
|
||||
$value = str_replace(' ', '%20', trim($value));
|
||||
$value = (string) preg_replace('|[^a-z0-9-~+_.?#=!&;,/:%@$\|*\'()\[\]\x80-\xff]|i', '', $value);
|
||||
do {
|
||||
$before = $value;
|
||||
$value = str_replace(['%0d', '%0a', '%0D', '%0A'], '', $value);
|
||||
} while ($value !== $before);
|
||||
|
||||
return str_starts_with(strtolower($value), 'javascript:') ? '' : $value;
|
||||
}
|
||||
|
||||
public function allowsUnfilteredHtml(): bool
|
||||
{
|
||||
return $this->unfiltered;
|
||||
}
|
||||
|
||||
/** Every post is readable but 666 (ruling S37, select_post settings). */
|
||||
public function canReadPost(int $id): bool
|
||||
{
|
||||
return $id !== 666;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static function media(): MediaResolver
|
||||
{
|
||||
return new class implements MediaResolver {
|
||||
public function image(int $id): ?array
|
||||
{
|
||||
return match ($id) {
|
||||
33 => ['url' => 'https://kunde.test/wp-content/uploads/team.jpg', 'alt' => 'Unser Team'],
|
||||
12 => ['url' => 'https://kunde.test/wp-content/uploads/halle.jpg', 'alt' => ''],
|
||||
// Alt texts carrying a reference, whole or NUL-split (ruling S33b: dropped).
|
||||
44 => ['url' => 'https://kunde.test/wp-content/uploads/a.jpg', 'alt' => ComposeFixture::REFERENCE_ALT],
|
||||
// "<" + VT/FF: strip_tags keeps it as text – a later strip would build the tag (S32a).
|
||||
46 => ['url' => 'https://kunde.test/wp-content/uploads/c.jpg', 'alt' => '<' . chr(11)
|
||||
. 'img src=x onerror=alert(1)>Team'],
|
||||
45 => ['url' => 'https://kunde.test/wp-content/uploads/b.jpg', 'alt' => '$variable' . chr(0)
|
||||
. substr(ComposeFixture::REFERENCE_ALT, strlen('$variable'))],
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static function placeholders(): Placeholders
|
||||
{
|
||||
return new Placeholders(DesignSystemTest::sample());
|
||||
}
|
||||
|
||||
public static function attrWriter(bool $unfiltered = false): AttrWriter
|
||||
{
|
||||
return new AttrWriter(ModuleCatalogTest::sampleCatalog(), self::placeholders(), self::sanitizer($unfiltered));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, array<string, array<string, mixed>>>|null $modules Short form per
|
||||
* type instead of ShortformMap (to reach paths the curated map never names).
|
||||
*/
|
||||
public static function shortform(bool $unfiltered = false, ?array $modules = null): ShortformWriter
|
||||
{
|
||||
return new ShortformWriter(self::placeholders(), self::media(), self::attrWriter($unfiltered), $modules);
|
||||
}
|
||||
|
||||
public static function rules(): StructureRules
|
||||
{
|
||||
return new StructureRules(ModuleCatalogTest::sampleCatalog());
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder version 5.2.1. $design replaces the sample design system (one Placeholders instance
|
||||
* shared by AttrWriter and ShortformWriter, like the live wiring – ruling S31).
|
||||
*/
|
||||
public static function composer(bool $unfiltered = false, ?DesignSystem $design = null): Composer
|
||||
{
|
||||
$design ??= DesignSystemTest::sample();
|
||||
$placeholders = new Placeholders($design);
|
||||
$attrWriter = new AttrWriter(ModuleCatalogTest::sampleCatalog(), $placeholders, self::sanitizer($unfiltered));
|
||||
|
||||
return new Composer(
|
||||
ModuleCatalogTest::sampleCatalog(),
|
||||
self::rules(),
|
||||
new ShortformWriter($placeholders, self::media(), $attrWriter),
|
||||
$attrWriter,
|
||||
$design,
|
||||
'5.2.1'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -11,8 +11,8 @@ use LIT\MCP\Integrations\Divi\BlockTree\Parser;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\Serializer;
|
||||
use LIT\MCP\Integrations\Divi\Compose\Composer;
|
||||
use LIT\MCP\Integrations\Divi\Design\DesignSystem;
|
||||
use LIT\MCP\Tests\Unit\Integrations\Divi\Design\DesignSystemTest;
|
||||
use LIT\MCP\Tests\Unit\Integrations\Divi\Schema\ModuleCatalogTest;
|
||||
use LIT\MCP\Tests\Support\ComposeFixture;
|
||||
use LIT\MCP\Tests\Support\DiviFixtures;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class ComposerTest extends TestCase
|
||||
@@ -442,11 +442,11 @@ final class ComposerTest extends TestCase
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
|
||||
new Composer(
|
||||
ModuleCatalogTest::sampleCatalog(),
|
||||
DiviFixtures::catalog(),
|
||||
ComposeFixture::rules(),
|
||||
ComposeFixture::shortform(),
|
||||
ComposeFixture::attrWriter(),
|
||||
DesignSystemTest::sample(),
|
||||
DiviFixtures::designSystem(),
|
||||
'5.2.1"},"x":{"'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace LIT\MCP\Tests\Unit\Integrations\Divi\Compose;
|
||||
use InvalidArgumentException;
|
||||
use LIT\MCP\Integrations\Divi\Compose\Placeholders;
|
||||
use LIT\MCP\Integrations\Divi\Design\DesignSystem;
|
||||
use LIT\MCP\Tests\Support\ComposeFixture;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class PlaceholdersTest extends TestCase
|
||||
|
||||
@@ -5,6 +5,8 @@ declare(strict_types=1);
|
||||
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Compose;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use LIT\MCP\Tests\Support\ComposeFixture;
|
||||
use LIT\MCP\Tests\Support\DiviFixtures;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class ShortformWriterTest extends TestCase
|
||||
@@ -146,7 +148,7 @@ final class ShortformWriterTest extends TestCase
|
||||
public static function malicious(): array
|
||||
{
|
||||
$rows = [];
|
||||
foreach (VariableRefTest::malicious() as $name => [$value]) {
|
||||
foreach (DiviFixtures::maliciousRefs() as $name => [$value]) {
|
||||
foreach (array_keys(self::TARGETS) as $kind) {
|
||||
$rows[$kind . ': ' . $name] = [$kind, $value, $kind !== 'text'];
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Compose;
|
||||
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\Block;
|
||||
use LIT\MCP\Tests\Support\ComposeFixture;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class StructureRulesTest extends TestCase
|
||||
|
||||
@@ -10,6 +10,7 @@ use LIT\MCP\Integrations\Divi\BlockTree\NodeIndex;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\Parser;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\Serializer;
|
||||
use LIT\MCP\Integrations\Divi\Compose\TreeEditor;
|
||||
use LIT\MCP\Tests\Support\ComposeFixture;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class TreeEditorTest extends TestCase
|
||||
|
||||
@@ -6,7 +6,8 @@ namespace LIT\MCP\Tests\Unit\Integrations\Divi\Compose;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use LIT\MCP\Integrations\Divi\Compose\VariableRef;
|
||||
use LIT\MCP\Tests\Unit\Integrations\Divi\Design\DesignSystemTest;
|
||||
use LIT\MCP\Tests\Support\ComposeFixture;
|
||||
use LIT\MCP\Tests\Support\DiviFixtures;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/** Ruling S1/S2/S12/S21/S22: every $variable(…)$ value is parsed, strictly checked, cleaned and re-serialized. */
|
||||
@@ -15,7 +16,7 @@ final class VariableRefTest extends TestCase
|
||||
/** @param array<string, array<string, string>>|null $dynamicContent */
|
||||
private static function ref(?array $dynamicContent = null): VariableRef
|
||||
{
|
||||
return new VariableRef(DesignSystemTest::sample(), $dynamicContent);
|
||||
return new VariableRef(DiviFixtures::designSystem(), $dynamicContent);
|
||||
}
|
||||
|
||||
private static function content(string $name, string $settings): string
|
||||
@@ -125,65 +126,7 @@ final class VariableRefTest extends TestCase
|
||||
*/
|
||||
public static function malicious(): array
|
||||
{
|
||||
$end = self::escape('0029') . '$';
|
||||
|
||||
return [
|
||||
'escaped end marker in before' => [self::content('post_title', '{"before":"x' . $end . ' <b>y</b>","after":""}'), 'allein im Wert'],
|
||||
'escaped end marker in after' => [self::content('post_title', '{"before":"","after":"' . $end . '"}'), 'allein im Wert'],
|
||||
'escaped end marker in date format' => [self::content('post_date', '{"custom_date_format":"d' . $end . '"}'), 'allein im Wert'],
|
||||
'escaped end marker in name' => [self::content('post_title' . $end, '{}'), 'Unbekannter Name'],
|
||||
'select with markup' => [self::content('post_date', '{"date_format":"<b>d</b>"}'), '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' => [self::content('gvid-nope', '{}'), 'gvid-nope'],
|
||||
'content gvid as color' => ['$variable({"type":"color","value":{"name":"gvid-s1","settings":{}}})$', 'gvid-s1'],
|
||||
'color gcid as content' => [self::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;}</style><script>alert(1)</script>","settings":{}}})$',
|
||||
'Name',
|
||||
],
|
||||
'settings on a global variable' => [self::content('gvid-s1', '{"before":"<b>x</b>"}'), 'before'],
|
||||
'settings on a theme font' => [self::content('--et_global_heading_font', '{"after":""}'), 'after'],
|
||||
'unknown settings key' => [self::content('post_title', '{"before":"","foo":"1"}'), 'foo'],
|
||||
'setting the option lacks' => [self::content('post_featured_image', '{"before":"x"}'), 'before'],
|
||||
'enable_html' => [self::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;}</style>"}}})$',
|
||||
'opacity',
|
||||
],
|
||||
'protected meta key' => [self::content('custom_meta__edit_lock', '{}'), 'custom_meta__edit_lock'],
|
||||
'doubled meta prefix' => [
|
||||
self::content('custom_meta_custom_meta__edit_lock', '{}'),
|
||||
'custom_meta_custom_meta__edit_lock',
|
||||
],
|
||||
'loop content outside a loop' => [self::content('loop_post_title', '{}'), 'loop_post_title'],
|
||||
'unknown dynamic content' => [self::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":"})$<script>alert(1)</script>"})$', 'allein im Wert'],
|
||||
'nested reference in before' => [self::content('post_title', '{"before":"$variable(x)$","after":""}'), 'allein im Wert'],
|
||||
// S28: a marker the sanitizer rebuilds inside a cleaned setting (html(), text()).
|
||||
'tag-split marker in before' => [self::content('post_title', '{"before":"$variable<x>(y","after":""}'), 'allein im Wert'],
|
||||
'NUL-split marker in after' => [
|
||||
self::content('post_title', '{"before":"","after":"$variable' . self::escape('0000') . '(y"}'),
|
||||
'allein im Wert',
|
||||
],
|
||||
'octet-split marker in date format' => [self::content('post_date', '{"custom_date_format":"$variable%41(y"}'), 'allein im Wert'],
|
||||
// S32: text() keeps control characters (like sanitize_text_field); VariableRef strips them.
|
||||
'NUL-split marker in date format' => [
|
||||
self::content('post_date', '{"custom_date_format":"$variable' . self::escape('0000') . '(y"}'),
|
||||
'allein im Wert',
|
||||
],
|
||||
'DEL-split marker in date format' => [
|
||||
self::content('post_date', '{"custom_date_format":"$variable' . self::escape('007f') . '(y"}'),
|
||||
'allein im Wert',
|
||||
],
|
||||
'nested marker in before' => [self::content('post_title', '{"before":"$variable(","after":""}'), 'allein im Wert'],
|
||||
'broken json' => ['$variable(kaputt)$', 'JSON'],
|
||||
'trailing text' => [self::content('post_title', '{}') . ' ', 'allein im Wert'],
|
||||
'embedded in text' => ['<p>Hi ' . self::content('post_title', '{"before":"","after":""}') . '</p>', 'allein im Wert'],
|
||||
];
|
||||
return DiviFixtures::maliciousRefs();
|
||||
}
|
||||
|
||||
/** @dataProvider malicious */
|
||||
|
||||
@@ -7,7 +7,8 @@ namespace LIT\MCP\Tests\Unit\Integrations\Divi\Compose;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\Document;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\NodeIndex;
|
||||
use LIT\MCP\Integrations\Divi\Compose\Warnings;
|
||||
use LIT\MCP\Tests\Unit\Integrations\Divi\Design\DesignSystemTest;
|
||||
use LIT\MCP\Tests\Support\ComposeFixture;
|
||||
use LIT\MCP\Tests\Support\DiviFixtures;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class WarningsTest extends TestCase
|
||||
@@ -34,7 +35,7 @@ final class WarningsTest extends TestCase
|
||||
$document->append($block);
|
||||
}
|
||||
|
||||
$warnings = (new Warnings(DesignSystemTest::sample()))->collect(NodeIndex::build($document), $composed['blocks']);
|
||||
$warnings = (new Warnings(DiviFixtures::designSystem()))->collect(NodeIndex::build($document), $composed['blocks']);
|
||||
|
||||
self::assertSame(
|
||||
[
|
||||
@@ -50,6 +51,6 @@ final class WarningsTest extends TestCase
|
||||
$warnings
|
||||
);
|
||||
|
||||
self::assertSame([], (new Warnings(DesignSystemTest::sample()))->collect(NodeIndex::build($document), []));
|
||||
self::assertSame([], (new Warnings(DiviFixtures::designSystem()))->collect(NodeIndex::build($document), []));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user