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:
@@ -10,9 +10,10 @@ defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* Posts (pages, Theme Builder layouts, other library entries) that show library entry $id: every
|
||||
* living post whose content has "globalModule":"<id>" – Divi's own way to find instances (string
|
||||
* scan, BlockParserStore, F10). One query per report (ID, status and type only – never the content,
|
||||
* audit L1); the rights of the caller are then checked in chunks with a bounded post cache:
|
||||
* living post whose content references it in any form Divi renders (rows(), ruling P23 – Divi 5
|
||||
* instances in any JSON spelling, Divi 4 global_module shortcodes; fail closed). Candidates come
|
||||
* from one query (ID, status and type), their content is parsed in bounded chunks (audit L1); the
|
||||
* rights of the caller are then checked in chunks with a bounded post cache:
|
||||
* - readable posts are listed (at most LIMIT, with title); unreadable ones only count (audit L4);
|
||||
* - live posts (ruling P8a) the caller may not edit_post block a confirmed edit – by ID when
|
||||
* readable, else as a count; Theme Builder layouts also without Divi's theme_builder role option
|
||||
@@ -33,6 +34,12 @@ class GlobalUsages
|
||||
/** Posts whose rights are checked with one primed cache. */
|
||||
public const CHUNK = 200;
|
||||
|
||||
/** Candidates whose content is read and parsed per call; the rest count as references (ruling P23). */
|
||||
public const PARSE_CAP = 5000;
|
||||
|
||||
/** Candidate contents read per query (bounded memory, audit L1). */
|
||||
public const CONTENT_CHUNK = 50;
|
||||
|
||||
/** Posts that render now or will on their own: published, private, scheduled (ruling P8a). */
|
||||
public const LIVE = ['publish', 'private', 'future'];
|
||||
|
||||
@@ -118,22 +125,45 @@ class GlobalUsages
|
||||
}
|
||||
|
||||
/**
|
||||
* Every post (no revision, not trashed) that shows $libraryId, by ID – one query, no content. Like
|
||||
* Divi's render, "globalModule":"0123" names entry 123 (hardening); the LIKE only narrows the
|
||||
* rows the REGEXP looks at.
|
||||
* Every post (no revision, not trashed) that shows $libraryId (ruling P23, fail closed). The
|
||||
* candidates come from one query (ID, status and type only): content with "globalModule", a
|
||||
* Divi 4 "global_module" or a JSON escape of a letter (an escaped key such as globalModule
|
||||
* always has one). Their content is then read in chunks of CONTENT_CHUNK and parsed by
|
||||
* UsageMatcher; beyond PARSE_CAP candidates the rest counts as references unread (audit L1).
|
||||
*
|
||||
* @return list<array{id: int, status: string, type: string}>
|
||||
*/
|
||||
public function rows(int $libraryId): array
|
||||
{
|
||||
$candidates = $this->candidates();
|
||||
$result = [];
|
||||
foreach (array_chunk(array_slice($candidates, 0, self::PARSE_CAP), self::CONTENT_CHUNK) as $chunk) {
|
||||
$contents = $this->contents(array_map(static fn (array $row): int => $row['id'], $chunk));
|
||||
foreach ($chunk as $row) {
|
||||
// A candidate whose content could not be read counts too (fail closed).
|
||||
if (! isset($contents[$row['id']]) || UsageMatcher::shows($contents[$row['id']], $libraryId)) {
|
||||
$result[] = $row;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return array_merge($result, array_slice($candidates, self::PARSE_CAP));
|
||||
}
|
||||
|
||||
/** @return list<array{id: int, status: string, type: string}> */
|
||||
private function candidates(): array
|
||||
{
|
||||
$db = $this->db();
|
||||
$escapedLetter = str_repeat(chr(92), 2) . 'u00[4-7][0-9a-fA-F]';
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery
|
||||
$rows = $db->get_results(
|
||||
$db->prepare(
|
||||
'SELECT ID, post_status, post_type FROM ' . $db->posts . ' WHERE post_content LIKE %s AND '
|
||||
. "post_content REGEXP %s AND post_type <> 'revision' AND " . self::LISTED . ' ORDER BY ID ASC',
|
||||
'%' . $db->esc_like('"globalModule":"') . '%',
|
||||
'"globalModule":"0*' . $libraryId . '"'
|
||||
'SELECT ID, post_status, post_type FROM ' . $db->posts . ' WHERE (post_content LIKE %s OR '
|
||||
. "post_content LIKE %s OR post_content REGEXP %s) AND post_type <> 'revision' AND "
|
||||
. self::LISTED . ' ORDER BY ID ASC',
|
||||
'%' . $db->esc_like('globalModule') . '%',
|
||||
'%' . $db->esc_like('global_module') . '%',
|
||||
$escapedLetter
|
||||
),
|
||||
'ARRAY_A'
|
||||
);
|
||||
@@ -151,6 +181,32 @@ class GlobalUsages
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<int> $ids
|
||||
* @return array<int, string> Content by ID.
|
||||
*/
|
||||
private function contents(array $ids): array
|
||||
{
|
||||
$db = $this->db();
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery
|
||||
$rows = $db->get_results(
|
||||
$db->prepare(
|
||||
'SELECT ID, post_content FROM ' . $db->posts . ' WHERE ID IN ('
|
||||
. implode(', ', array_fill(0, count($ids), '%d')) . ')',
|
||||
$ids
|
||||
),
|
||||
'ARRAY_A'
|
||||
);
|
||||
// phpcs:enable
|
||||
|
||||
$contents = [];
|
||||
foreach (is_array($rows) ? $rows : [] as $row) {
|
||||
$contents[(int) $row['ID']] = (string) $row['post_content'];
|
||||
}
|
||||
|
||||
return $contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ruling P8a: whether the caller may edit_post the live post $row; a Theme Builder layout or
|
||||
* template additionally needs Divi's role option theme_builder (audit L6 – as Divi's own
|
||||
|
||||
Reference in New Issue
Block a user