fix: GlobalUsages finds every reference Divi renders, fail closed (F1, ruling P23)

Candidates come from one ID/status/type query: content with globalModule,
global_module or a JSON-escaped letter. Their content is read in chunks of
50 and parsed by UsageMatcher: Divi 5 openers are json-decoded (escaped keys
and values, leading zeros, any depth), and Divi 4 global_module attributes
are read in any quoting. An undecodable escaped opener counts as a possible
reference. Beyond 5000 candidates the rest count unread. The guide names
the formats and the wp_block parity. Also fixes the grammar of the
blocked-hidden count.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-27 04:42:46 +02:00
co-authored by Claude Opus 5.5
parent f1937ca6ff
commit fec5d9da7b
7 changed files with 299 additions and 34 deletions
@@ -342,7 +342,7 @@ final class GlobalEditsTest extends TestCase
self::assertStringNotContainsString('1081', (string) wp_json_encode($data));
self::assertToolError($refused, 'forbidden');
self::assertStringNotContainsString('1081', $refused->get_error_message());
self::assertStringContainsString('nicht lesen darf', $refused->get_error_message());
self::assertStringContainsString('1 Eintrag, den dieses Konto nicht lesen darf', $refused->get_error_message());
self::assertSame([], $this->saved);
}
@@ -13,37 +13,78 @@ use WP_Post;
final class GlobalUsagesTest extends TestCase
{
/** Audit L1: one query, ID, status and type only – never the content, no COUNT, no second scan. */
public function testFindsInstancesLikeDiviWithOneQueryWithoutContent(): void
/**
* Ruling P23 (re-review F1): candidates by a wide, fail-closed prefilter (ID, status and type
* only), then their content in chunks, parsed in PHP (UsageMatcher) – every format Divi renders.
*/
public function testFindsEveryFormatDiviRendersFromCandidates(): void
{
$bs = chr(92);
$contents = [
1053 => '<!-- wp:divi/global-layout {"globalModule":"1058","blockName":"divi/text"} /-->',
1054 => '[et_pb_section global_module="01058"][/et_pb_section]',
1055 => '<!-- wp:divi/global-layout {"' . $bs . 'u0067lobalModule":"1058"} /-->',
1056 => '<!-- wp:divi/global-layout {"globalModule":"' . $bs . 'u00310' . '58"} /-->',
1057 => '<!-- wp:divi/global-layout {"globalModule":"1059"} /-->',
1061 => '<!-- wp:divi/global-layout {"' . $bs . 'u0067lobalModule":"1058",} /-->',
];
$db = Mockery::mock(\wpdb::class)->makePartial();
$queries = [];
$db->allows('get_results')->andReturnUsing(static function (string $sql) use (&$queries): array {
$db->allows('get_results')->andReturnUsing(static function (string $sql) use (&$queries, $contents): array {
$queries[] = $sql;
if (str_contains($sql, 'post_content FROM')) {
preg_match('/IN \(([0-9, ]+)\)/', $sql, $in);
$ids = array_map('intval', explode(',', $in[1]));
return [
['ID' => '1053', 'post_status' => 'draft', 'post_type' => 'page'],
['ID' => '1061', 'post_status' => 'private', 'post_type' => 'et_header_layout'],
];
return array_map(static fn (int $id): array => ['ID' => (string) $id, 'post_content' => $contents[$id]], $ids);
}
return array_map(
static fn (int $id): array => ['ID' => (string) $id, 'post_status' => 'publish', 'post_type' => 'page'],
array_keys($contents)
);
});
$rows = (new GlobalUsages($db))->rows(1058);
self::assertSame(
[
['id' => 1053, 'status' => 'draft', 'type' => 'page'],
['id' => 1061, 'status' => 'private', 'type' => 'et_header_layout'],
],
$rows
);
self::assertCount(1, $queries);
self::assertSame([1053, 1054, 1055, 1056, 1061], array_column($rows, 'id'), '1061: undecodable, fail closed');
self::assertStringStartsWith('SELECT ID, post_status, post_type FROM', $queries[0]);
self::assertStringContainsString("post_content LIKE '%\"globalModule\":\"%'", $queries[0]);
self::assertStringContainsString("post_content REGEXP '\"globalModule\":\"0*1058\"'", $queries[0]);
self::assertStringNotContainsString('post_content,', $queries[0]);
self::assertStringContainsString("post_content LIKE '%globalModule%'", $queries[0]);
self::assertStringContainsString("post_content LIKE '%global" . $bs . "_module%'", $queries[0]);
self::assertStringContainsString("post_content REGEXP '" . $bs . $bs . 'u00[4-7][0-9a-fA-F]' . "'", $queries[0]);
self::assertStringContainsString("post_status NOT IN ('trash', 'auto-draft', 'inherit')", $queries[0]);
self::assertStringContainsString("post_type <> 'revision'", $queries[0]);
self::assertStringNotContainsString('LIMIT', $queries[0]);
self::assertStringNotContainsString('post_content,', $queries[0]);
self::assertCount(2, $queries, 'one candidate query, one content chunk');
}
/** Audit L1: contents are read in chunks; beyond the parse cap candidates count as references. */
public function testReadsContentsInChunksAndFailsClosedBeyondTheCap(): void
{
$total = GlobalUsages::PARSE_CAP + 3;
$db = Mockery::mock(\wpdb::class)->makePartial();
$chunks = 0;
$db->allows('get_results')->andReturnUsing(static function (string $sql) use (&$chunks, $total): array {
if (str_contains($sql, 'post_content FROM')) {
++$chunks;
preg_match('/IN \(([0-9, ]+)\)/', $sql, $in);
return array_map(
static fn (string $id): array => ['ID' => trim($id), 'post_content' => 'nichts'],
explode(',', $in[1])
);
}
return array_map(
static fn (int $id): array => ['ID' => (string) $id, 'post_status' => 'draft', 'post_type' => 'page'],
range(1, $total)
);
});
$rows = (new GlobalUsages($db))->rows(1058);
self::assertSame((int) ceil(GlobalUsages::PARSE_CAP / GlobalUsages::CONTENT_CHUNK), $chunks);
self::assertSame(range(GlobalUsages::PARSE_CAP + 1, $total), array_column($rows, 'id'));
}
/** Audit L4: readable posts with title, unreadable ones only counted. */
@@ -0,0 +1,78 @@
<?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],
];
}
/** @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]));
}
}