" 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 shortcodes – in the content outside block openers and in every decoded string value of an * opener (Divi runs them there too): attributes parsed like shortcode_parse_atts(), global_module * cast with (int) like Divi; a non-empty value other than "0" that casts to 0 makes Divi render the * newest layout (et_pb_load_global_module, WP_Query p=0), so it counts for EVERY entry; * - an opener with JSON escapes (or a globalModule/global_module) that cannot be decoded, or content * the regex engine cannot scan, counts as a possible reference. * referencesAny() asks the same for every entry at once (ruling Q7): content that becomes a global entry * must not render another entry – nor, through a value that casts to 0, the newest layout, which may be * that entry itself. * Pure PHP. */ final class UsageMatcher { /** A block opener and its attribute JSON (the Parser's delimiter grammar). */ private const OPENER = '/).)*+)?})\s+\/?-->/s'; /** An opening shortcode tag and its attribute text (get_shortcode_regex(), any tag name). */ private const TAG = '/\[\[?([\w-]+)(?![\w-])([^\]\/]*(?:\/(?!\])[^\]\/]*)*?)\/?\]/'; /** WordPress' get_shortcode_atts_regex(). */ private const ATTS = '/([\w-]+)\s*=\s*"([^"]*)"(?:\s|$)|([\w-]+)\s*=\s*\'([^\']*)\'(?:\s|$)|([\w-]+)\s*=\s*' . '([^\s\'"]+)(?:\s|$)|"([^"]*)"(?:\s|$)|\'([^\']*)\'(?:\s|$)|(\S+)(?:\s|$)/'; public static function shows(string $content, int $id): bool { return self::matches($content, $id); } /** * Whether $content renders any library entry, in every spelling shows() knows (ruling Q7): a * globalModule key with any value, a global_module attribute Divi does not skip; fail closed. */ public static function referencesAny(string $content): bool { return self::matches($content, null); } /** shows() for entry $id, or for any entry when $id is null. */ private static function matches(string $content, ?int $id): bool { $textual = preg_match( $id === null ? '/"globalModule"\s*:/' : '/"globalModule":"0*' . $id . '"/', $content ); if ($textual !== 0) { return true; // A match, or a regex failure (fail closed). } $found = preg_match_all(self::OPENER, $content, $matches); if ($found === false) { return true; } // Shortcodes outside the openers (Divi 4 pages, free text between blocks). $outside = $found === 0 ? $content : str_replace($matches[1], '', $content); if (self::shortcodeShows($outside, $id)) { return true; } $escape = chr(92) . 'u'; foreach ($matches[1] as $json) { if ( ! str_contains($json, $escape) && ! str_contains($json, 'globalModule') && stripos($json, 'global_module') === false ) { continue; } $attrs = json_decode($json, true); if (! is_array($attrs) || self::names($attrs, $id)) { return true; } foreach (self::strings($attrs) as $value) { if (self::shortcodeShows($value, $id)) { return true; } } } return false; } /** Whether a Divi 4 shortcode in $text renders entry $id (null: any entry); a value casting to 0 renders any. */ private static function shortcodeShows(string $text, ?int $id): bool { if (stripos($text, 'global_module') === false) { return false; } $found = preg_match_all(self::TAG, $text, $tags); if ($found === false) { return true; } foreach ($tags[2] as $attributes) { $atts = self::parseAtts($attributes); if ($atts === null) { return true; } $value = $atts['global_module'] ?? null; // Divi: ! empty( $global_module_id ), then WP_Query p => (int) $global_module_id. if (! is_string($value) || $value === '' || $value === '0') { continue; } $cast = (int) $value; if ($id === null || $cast === 0 || $cast === $id) { return true; } } return false; } /** * shortcode_parse_atts() of WordPress 6.9 (named attributes only); null when PCRE fails. * * @return array|null */ private static function parseAtts(string $text): ?array { $text = preg_replace('/[\x{00a0}\x{200b}]+/u', ' ', $text); if (! is_string($text)) { return null; } $found = preg_match_all(self::ATTS, $text, $matches, PREG_SET_ORDER); if ($found === false) { return null; } $atts = []; foreach ($matches as $m) { foreach ([1, 3, 5] as $key) { if (! empty($m[$key])) { $atts[strtolower($m[$key])] = stripcslashes($m[$key + 1]); break; } } } // WordPress rejects values with unclosed HTML elements. foreach ($atts as $name => $value) { if (str_contains($value, '<') && preg_match('/^[^<]*+(?:<[^>]*+>[^<]*+)*+$/', $value) !== 1) { $atts[$name] = ''; } } return $atts; } /** * @param array $attrs Whether a globalModule key at any depth names $id (null: any * globalModule key, whatever its value – fail closed). */ private static function names(array $attrs, ?int $id): bool { foreach ($attrs as $key => $value) { if ($id === null && $key === 'globalModule') { return true; } 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; } /** * @param array $attrs * @return list Every string value at any depth. */ private static function strings(array $attrs): array { $strings = []; array_walk_recursive($attrs, static function (mixed $value) use (&$strings): void { if (is_string($value)) { $strings[] = $value; } }); return $strings; } }