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:
@@ -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. */
|
||||
|
||||
Reference in New Issue
Block a user