Files
WP-LIT-MCP/src/Integrations/Divi/BlockTree/Fingerprint.php
T
janniclas.loosenandClaude Opus 5.5 4e959833b9 fix: identical means identical bytes – text parts count, pages and posts only (ruling Q25)
- Fingerprint::content() includes non-whitespace text parts (raw HTML between
  blocks, inner HTML of core blocks) and opaque blocks with all their bytes;
  structure() unchanged; find-similar "identical" follows
- make-global mode 2 also compares Fingerprint::canonical() bytes of each node
- sources and targets: post types page and post only (POST_TYPES)
- a rolled-back page N>1 keeps DiviSaver's "Revision X bleibt erhalten"
- guide: escaped divi/global-layout re-expanded by Divi's do_blocks in
  shortcode-module and WooCommerce text output documented as residual risk

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
2026-09-27 21:59:20 +02:00

112 lines
3.8 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\BlockTree;
use stdClass;
defined('ABSPATH') || exit;
/**
* Fingerprints of a node (E11) for lit/library-find-similar, the "similar section" warning (spec §5.5) and
* lit/library-make-global. structure(): block names and nesting, column layout (flexType) and preset IDs – two
* sections built alike with other texts share it. content(): every attribute but the admin label,
* builderVersion and globalParent, children and every non-whitespace text part included – "the same section"
* (ruling Q25: raw HTML between blocks and the inner HTML of core blocks are visible too). Whitespace between
* blocks never counts; an opaque block counts with all its bytes (content) or by its effective type (structure).
* Pure PHP.
*/
final class Fingerprint
{
/** Top-level attributes that say nothing about what visitors see. */
private const IGNORED = ['builderVersion', 'globalParent'];
public static function structure(Block $block): string
{
return self::hash(self::shape($block));
}
public static function content(Block $block): string
{
return self::hash(self::body($block));
}
/**
* The normalized bytes content() hashes (ruling Q25): block names, attributes with sorted keys (without admin
* label, builderVersion, globalParent), every non-whitespace text part – raw HTML or shortcodes between blocks
* and the inner HTML of core blocks – and opaque blocks with all their bytes. Two nodes with the same
* canonical() look the same to visitors; lit/library-make-global compares these bytes, not only the hash.
*/
public static function canonical(Block $block): string
{
return self::body($block);
}
private static function shape(Block $block): string
{
if ($block->isOpaque()) {
return 'opaque:' . GlobalInstance::effectiveName($block);
}
$presets = NodeSummary::presetIds($block);
sort($presets);
$children = array_map(self::shape(...), $block->children());
return $block->name() . '|' . (NodeSummary::columnWidth($block) ?? '') . '|' . implode(',', $presets)
. '[' . implode(';', $children) . ']';
}
private static function body(Block $block): string
{
if ($block->isOpaque()) {
return 'opaque:' . self::text(Serializer::block($block));
}
$attrs = $block->attrs();
foreach (self::IGNORED as $key) {
unset($attrs[$key]);
}
if (is_array($attrs['module']['meta'] ?? null)) {
unset($attrs['module']['meta']['adminLabel']);
}
$parts = [];
foreach ($block->isVoid() ? [] : $block->parts() as $part) {
if ($part instanceof Block) {
$parts[] = self::body($part);
} elseif (trim($part) !== '') {
$parts[] = 'text:' . Json::encode(self::text($part));
}
}
return $block->name() . Json::encode(self::sorted($attrs)) . '[' . implode(';', $parts) . ']';
}
/** Text as visitors get it: line breaks unified, whitespace at both ends dropped. */
private static function text(string $text): string
{
return trim(str_replace("\r\n", "\n", $text));
}
/** Object keys sorted at every depth, so the key order Divi or the plugin wrote does not count. */
private static function sorted(mixed $value): mixed
{
if ($value instanceof stdClass) {
$value = (array) $value;
}
if (! is_array($value)) {
return $value;
}
if (! array_is_list($value)) {
ksort($value);
}
return array_map(self::sorted(...), $value);
}
private static function hash(string $value): string
{
return substr(hash('sha256', $value), 0, 16);
}
}