Files
WP-LIT-MCP/tests/Unit/Integrations/Divi/Compose/ComposeFixture.php
T
janniclas.loosenandClaude Opus 5.5 f86cd5b34a feat: Divi short-form writer
ShortformWriter routes every short-form string through AttrWriter::cleanValue()
with the curated field kind (new optional kind override), exposes the color
check for background_color and guards builder-only paths on removal too.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
2026-09-26 13:59:24 +02:00

117 lines
4.3 KiB
PHP

<?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\MediaResolver;
use LIT\MCP\Integrations\Divi\Compose\Placeholders;
use LIT\MCP\Integrations\Divi\Compose\ShortformWriter;
use LIT\MCP\Integrations\Divi\Compose\ValueSanitizer;
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
{
public static function sanitizer(bool $unfiltered = false): ValueSanitizer
{
return new class ($unfiltered) implements ValueSanitizer {
public function __construct(private bool $unfiltered)
{
}
/** Like sanitize_text_field: NUL bytes, tags and %XX octets go, then trim. */
public function text(string $value): string
{
$value = strip_tags(str_replace(chr(0), '', $value));
return trim((string) preg_replace('/%[a-f0-9]{2}/i', '', $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;
}
};
}
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' => ''],
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);
}
}