Files
WP-LIT-MCP/tests/Unit/Integrations/Divi/Library/UsageMatcherTest.php
T
janniclas.loosenandClaude Opus 5.5 b6d791d46a fix: UsageMatcher reads Divi 4 shortcodes like WordPress and Divi, everywhere (ruling P24)
- G1: every decoded string value of a Divi 5 opener is scanned for shortcodes;
  outside the openers only the remaining content is.
- G2: attributes are parsed like shortcode_parse_atts() and global_module is
  cast with (int), as Divi does.
- G3: a non-empty global_module other than "0" that casts to 0 makes Divi
  render the newest layout, so it counts for every entry (fail closed).
- The prefilter LIKEs use LOWER(post_content), so they are case-insensitive
  under any collation.
- The guide describes the fail-closed rule without an absolute claim.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-27 04:53:54 +02:00

124 lines
6.4 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Library;
use LIT\MCP\Integrations\Divi\Library\UsageMatcher;
use PHPUnit\Framework\TestCase;
/**
* Ruling P23 (re-review F1): every reference Divi renders counts – Divi 5 openers in any JSON
* spelling, Divi 4 shortcodes in any quoting; an escaped opener that cannot be decoded counts as a
* possible reference (fail closed). Escape bytes are built with chr(92), never written inline.
*/
final class UsageMatcherTest extends TestCase
{
private static function u(string $hex): string
{
return chr(92) . 'u' . $hex;
}
/** @return array<string, array{string, bool}> Content => whether it shows entry 123. */
public static function contents(): array
{
$escapedKey = '"' . self::u('0067') . 'lobalModule"';
return [
'divi 5 canonical' => ['<!-- wp:divi/global-layout {"globalModule":"123","blockName":"divi/text"} /-->', true],
'divi 5 leading zeros' => ['<!-- wp:divi/global-layout {"globalModule":"0123","blockName":"divi/text"} /-->', true],
'divi 5 escaped key' => ['<!-- wp:divi/global-layout {' . $escapedKey . ':"123","blockName":"divi/text"} /-->', true],
'divi 5 escaped value' => [
'<!-- wp:divi/global-layout {"globalModule":"' . self::u('0031') . '23","blockName":"divi/text"} /-->',
true,
],
'divi 5 escaped key with leading zeros' => [
'<!-- wp:divi/global-layout {' . $escapedKey . ':"00123"} /-->',
true,
],
'divi 5 another entry' => ['<!-- wp:divi/global-layout {"globalModule":"1234","blockName":"divi/text"} /-->', false],
'divi 5 escaped key, another entry' => ['<!-- wp:divi/global-layout {' . $escapedKey . ':"124"} /-->', false],
'divi 5 id in local attributes' => [
'<!-- wp:divi/global-layout {"globalModule":"5","localAttrs":{"x":{"globalModule":"123"}}} /-->',
true,
],
'divi 4 double quotes' => ['[et_pb_section global_module="123"][/et_pb_section]', true],
'divi 4 single quotes and zeros' => ["[et_pb_text global_module='0123']x[/et_pb_text]", true],
'divi 4 unquoted' => ['[et_pb_row global_module=123 admin_label="x"][/et_pb_row]', true],
'divi 4 typographic quotes' => ['[et_pb_row global_module=' . "\u{201D}" . '123' . "\u{201D}" . '][/et_pb_row]', true],
'divi 4 another entry' => ['[et_pb_section global_module="1230"][/et_pb_section]', false],
'undecodable escaped opener' => [
'<!-- wp:divi/global-layout {' . $escapedKey . ':"123",} /-->',
true,
],
'undecodable escaped opener naming nothing readable' => [
'<!-- wp:divi/text {' . self::u('0067') . 'kaputt} /-->',
true,
],
'escaped markup only' => [
'<!-- wp:divi/text {"content":{"innerContent":{"desktop":{"value":"' . self::u('003c') . 'p'
. self::u('003e') . 'x"}}}} /-->',
false,
],
'plain page' => ['<!-- wp:divi/section --><!-- /wp:divi/section -->', false],
// Ruling P24 G1: a Divi 4 shortcode inside a Divi 5 attribute value (quotes serialized as escapes).
'shortcode in a divi 5 text value' => [self::inText('[et_pb_section global_module=' . self::u('0022') . '123'
. self::u('0022') . '][/et_pb_section]'), true],
'shortcode in a divi 5 text value, another entry' => [self::inText('[et_pb_section global_module='
. self::u('0022') . '124' . self::u('0022') . '][/et_pb_section]'), false],
'shortcode in a divi 5 text value, escaped digits' => [self::inText('[et_pb_section global_module='
. self::u('0022') . self::u('0031') . self::u('0032') . '3' . self::u('0022') . '][/et_pb_section]'), true],
// G2: parsed like shortcode_parse_atts, cast with (int) like Divi.
'divi 4 leading space' => ['[et_pb_section global_module=" 123"][/et_pb_section]', true],
'divi 4 exponent' => ['[et_pb_section global_module="1.23e2"][/et_pb_section]', true],
'divi 4 upper-case attribute' => ['[et_pb_section GLOBAL_MODULE="123"][/et_pb_section]', true],
'divi 4 zero renders nothing' => ['[et_pb_section global_module="0"][/et_pb_section]', false],
'divi 4 empty renders nothing' => ['[et_pb_section global_module=""][/et_pb_section]', false],
'global_module as plain text' => ['<p>global_module="123" steht hier nur als Text</p>', false],
];
}
/** A Divi 5 text module whose content value holds $text, JSON-encoded like WordPress does. */
private static function inText(string $text): string
{
return '<!-- wp:divi/text {"content":{"innerContent":{"desktop":{"value":"' . $text . '"}}}} /-->';
}
/**
* Ruling P24 G3: a global_module Divi casts to 0 renders the newest library or TB layout
* (et_pb_load_global_module, WP_Query p=0) – a reference to every entry.
*
* @return array<string, array{string}>
*/
public static function wildcards(): array
{
return [
'entity before the id' => ['[et_pb_section global_module="&quot;123"][/et_pb_section]'],
'letters' => ['[et_pb_row global_module="abc"][/et_pb_row]'],
'typographic quotes' => ['[et_pb_row global_module=' . "\u{201D}" . '123' . "\u{201D}" . '][/et_pb_row]'],
'double zero' => ['[et_pb_row global_module="00"][/et_pb_row]'],
'inside a divi 5 value' => [self::inText('[et_pb_text global_module=' . self::u('0022') . 'x'
. self::u('0022') . ']a[/et_pb_text]')],
];
}
/** @dataProvider wildcards */
public function testAValueThatCastsToZeroShowsEveryEntry(string $content): void
{
self::assertTrue(UsageMatcher::shows($content, 123));
self::assertTrue(UsageMatcher::shows($content, 98765));
}
/** @dataProvider contents */
public function testFindsEveryReferenceDiviRenders(string $content, bool $shows): void
{
self::assertSame($shows, UsageMatcher::shows($content, 123));
}
public function testTheEscapeBytesAreReal(): void
{
self::assertSame(6, strlen(self::u('0067')));
self::assertSame(92, ord(self::u('0067')[0]));
}
}