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
@@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Library;
defined('ABSPATH') || exit;
/**
* Whether post content shows library entry $id (ruling P23, re-review F1), fail closed:
* - Divi 5: the textual "globalModule":"<digits>" Divi's render takes (leading zeros = intval), and
* every block opener whose JSON – in any spelling, escaped keys or values included – decodes to a
* globalModule with that ID at any depth;
* - Divi 4: a shortcode attribute global_module=<digits> in any quoting (intval);
* - an opener with JSON escapes (or a globalModule) that cannot be decoded, or content the regex
* engine cannot scan, counts as a possible reference.
* Pure PHP.
*/
final class UsageMatcher
{
/** A block opener and its attribute JSON (the Parser's delimiter grammar). */
private const OPENER = '/<!--\s+wp:(?:[a-z][a-z0-9_-]*\/)?[a-z][a-z0-9_-]*\s+'
. '({(?:(?:[^}]+|}+(?=})|(?!}\s+\/?-->).)*+)?})\s+\/?-->/s';
/** Divi 4: global_module=, any quote characters (straight, typographic, entities) before the digits. */
private const SHORTCODE = '/\bglobal_module\s*=\s*[^\d\s\]]{0,12}(\d+)/i';
public static function shows(string $content, int $id): bool
{
$textual = preg_match('/"globalModule":"0*' . $id . '"/', $content);
if ($textual !== 0) {
return true; // A match, or a regex failure (fail closed).
}
if (stripos($content, 'global_module') !== false) {
$found = preg_match_all(self::SHORTCODE, $content, $matches);
if ($found === false) {
return true;
}
foreach ($matches[1] as $value) {
if ((int) $value === $id) {
return true;
}
}
}
$escape = chr(92) . 'u';
if (! str_contains($content, 'globalModule') && ! str_contains($content, $escape)) {
return false;
}
$found = preg_match_all(self::OPENER, $content, $matches);
if ($found === false) {
return true;
}
foreach ($matches[1] as $json) {
if (! str_contains($json, $escape) && ! str_contains($json, 'globalModule')) {
continue;
}
$attrs = json_decode($json, true);
if (! is_array($attrs) || self::names($attrs, $id)) {
return true;
}
}
return false;
}
/** @param array<array-key, mixed> $attrs Whether a globalModule key at any depth names $id. */
private static function names(array $attrs, int $id): bool
{
foreach ($attrs as $key => $value) {
if (
$key === 'globalModule'
&& (is_int($value) || (is_string($value) && preg_match('/^\d+$/D', $value) === 1))
&& (int) $value === $id
) {
return true;
}
if (is_array($value) && self::names($value, $id)) {
return true;
}
}
return false;
}
}