feat: Signatur-Fingerabdruck der Divi-Aufrufe (Reflection); fingerprints.json enthält sig:-Einträge, divi-check --update-reference schreibt sie; Referenz für Divi 5.15.1

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Jan-Niclas Loosen
2026-10-18 22:01:56 +02:00
co-authored by Claude Sonnet 5.5
parent 392fe7c569
commit 710444e88d
8 changed files with 525 additions and 20 deletions
@@ -42,6 +42,19 @@ final class DiviAdapters
return $manifests;
}
/** @return list<Call> Every Divi call of every manifest, each symbol once (the first entry wins). */
public static function calls(): array
{
$calls = [];
foreach (self::manifests() as $manifest) {
foreach ($manifest->calls as $call) {
$calls[ltrim($call->symbol, '\\')] ??= $call;
}
}
return array_values($calls);
}
/** @return list<Mirror> */
public static function mirrors(): array
{
@@ -9,15 +9,22 @@ use RuntimeException;
defined('ABSPATH') || exit;
/**
* The expected fingerprints of the mirrors (fingerprints.json, shipped with the plugin) and their reference sources
* (tests/Fixtures/divi/reference/<id>.txt, development only – not in the plugin package). Written by
* wp lit-mcp divi-check --update-reference.
* The expected fingerprints (fingerprints.json, shipped with the plugin): the source fingerprint of every mirror under
* its ID, and the signature fingerprint of every Divi call under "sig:<symbol>" (SignatureFingerprint) – plus the
* reference sources of the mirrors (tests/Fixtures/divi/reference/<id>.txt, development only – not in the plugin
* package). Written by wp lit-mcp divi-check --update-reference.
*/
final class DiviReference
{
public const FILE = __DIR__ . '/fingerprints.json';
/** @return array<string, string> Mirror ID => sha256; [] when the file is missing or broken. */
/** Key prefix of the signature fingerprints; no mirror ID starts with it. */
public const SIGNATURE_PREFIX = 'sig:';
/**
* @return array<string, string> Mirror ID or "sig:<symbol>" => sha256, sorted by key; [] when the file is
* missing or broken.
*/
public static function expected(?string $file = null): array
{
$file ??= self::FILE;
@@ -40,7 +47,7 @@ final class DiviReference
}
/**
* @param array<string, string> $hashes Mirror ID => sha256.
* @param array<string, string> $hashes Mirror ID or "sig:<symbol>" => sha256; written sorted by key.
* @throws RuntimeException
*/
public static function writeExpected(array $hashes, ?string $file = null): void
@@ -54,6 +61,34 @@ final class DiviReference
}
}
/** The key of $call's signature fingerprint: "sig:" and the symbol without a leading backslash. */
public static function signatureKey(Call $call): string
{
return self::SIGNATURE_PREFIX . ltrim($call->symbol, '\\');
}
/**
* Signature fingerprints of the calls in the running PHP process (Divi loaded).
*
* @param list<Call>|null $calls null = DiviAdapters::calls().
* @return array<string, string> "sig:<symbol>" => sha256, sorted by key.
* @throws RuntimeException When a symbol is not loaded.
*/
public static function currentSignatures(?array $calls = null): array
{
$signatures = [];
foreach ($calls ?? DiviAdapters::calls() as $call) {
$hash = SignatureFingerprint::of($call);
if ($hash === null) {
throw new RuntimeException(sprintf('signature of %s: symbol is not loaded', $call->label()));
}
$signatures[self::signatureKey($call)] = $hash;
}
ksort($signatures);
return $signatures;
}
/** tests/Fixtures/divi/reference of a development checkout; null in an installed plugin. */
public static function sourceDir(): ?string
{
@@ -0,0 +1,99 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Release;
use ReflectionException;
use ReflectionFunction;
use ReflectionFunctionAbstract;
use ReflectionMethod;
use ReflectionParameter;
use ReflectionType;
defined('ABSPATH') || exit;
/**
* Fingerprint of the signature of one Divi function or static method (spec §4.2, E1 of the plan "Divi-Fähigkeiten"),
* read with Reflection from the running PHP process – so it needs the symbol to be loaded (in WordPress Divi is) and
* never includes a file. The class of a method is autoloaded the way ManifestCheck::accepts() already does it.
*
* What the hash covers: function or method; for a method its visibility and whether it is static; per parameter the
* type (nullable and union included; no type reads as "mixed"), by reference, variadic and whether it is optional
* (has a default); the return type (none reads as "mixed"). What it leaves out:
* - Parameter names: the plugin passes every argument to Divi positionally (PHP 8 named arguments are never used),
* so a rename cannot break a call, and a harmless rename must not lock a feature.
* - Default values: they are expressions (constants, arrays) that Reflection may be unable to evaluate in some
* requests and that change spelling without changing meaning; only "has a default" matters for the number of
* arguments the plugin may omit.
* - The body: a changed body is no change of the interface (mirrors cover the code the plugin repeats).
*/
final class SignatureFingerprint
{
/** sha256 of canonical(), null when the symbol does not exist or cannot be reflected. */
public static function of(Call $call): ?string
{
$canonical = self::canonical($call);
return $canonical === null ? null : hash('sha256', $canonical);
}
/**
* The text the hash is built from, e.g. "public static (int, array|string=, &mixed=, ...mixed): ?bool".
* Public for tests and diagnostics; null when the symbol does not exist.
*/
public static function canonical(Call $call): ?string
{
try {
$reflection = self::reflect($call);
} catch (ReflectionException) {
return null;
}
if ($reflection === null) {
return null;
}
return sprintf(
'%s (%s): %s',
self::kind($reflection),
implode(', ', array_map(self::parameter(...), $reflection->getParameters())),
self::type($reflection->getReturnType())
);
}
/** "function", or a method's visibility and "static" – a method that turns private or non-static breaks a call. */
private static function kind(ReflectionFunctionAbstract $reflection): string
{
if (! $reflection instanceof ReflectionMethod) {
return 'function';
}
$visibility = $reflection->isPublic() ? 'public' : ($reflection->isProtected() ? 'protected' : 'private');
return $reflection->isStatic() ? $visibility . ' static' : $visibility;
}
/** @throws ReflectionException */
private static function reflect(Call $call): ?ReflectionFunctionAbstract
{
$class = $call->className();
if ($class === null) {
return function_exists($call->member()) ? new ReflectionFunction($call->member()) : null;
}
return method_exists($class, $call->member()) ? new ReflectionMethod($class, $call->member()) : null;
}
private static function parameter(ReflectionParameter $parameter): string
{
return ($parameter->isPassedByReference() ? '&' : '')
. ($parameter->isVariadic() ? '...' : '')
. self::type($parameter->getType())
. ($parameter->isOptional() && ! $parameter->isVariadic() ? '=' : '');
}
private static function type(?ReflectionType $type): string
{
return $type === null ? 'mixed' : (string) $type;
}
}
@@ -1,6 +1,35 @@
{
"library-submit-layout": "594b3843d4c42e1a91e5eda06f108c79583a0036e46c92dd749f4f541e362d02",
"preset-sync": "9586f8005b8065893c0350977a58ee9171bf715fff21ac0c84663cc512f45522",
"sig:ET\\Builder\\Framework\\UserRole\\UserRole::can_current_user_use_visual_builder": "76d199c13adc6600c8a456225a8eb9db8037064f20eca9db597c4e45ccecf8d5",
"sig:ET\\Builder\\Packages\\Conversion\\Conversion::get_preset_attrs": "5b0ace397bc5e171b73b9c50df5d27942719dffe5344fb4bc36af0f35616a75b",
"sig:ET\\Builder\\Packages\\Conversion\\Conversion::get_preset_attrs_mapping": "b5c3403666ff92be1dd7bfa6e6589c32f6662b37282f2fd40922f8d50151745d",
"sig:ET\\Builder\\Packages\\GlobalData\\GlobalData::get_global_colors": "dfbf01f6750e3fa6c309f1200effcfda539d75c3404732cd5df939a8bff399ae",
"sig:ET\\Builder\\Packages\\GlobalData\\GlobalData::get_global_variables": "dfbf01f6750e3fa6c309f1200effcfda539d75c3404732cd5df939a8bff399ae",
"sig:ET\\Builder\\Packages\\GlobalData\\GlobalPreset::get_data": "dfbf01f6750e3fa6c309f1200effcfda539d75c3404732cd5df939a8bff399ae",
"sig:ET\\Builder\\Packages\\GlobalData\\GlobalPreset::get_deleted_preset_ids": "d36bfe7698116a94f7c6bc5ff00e3f66f3cd4267b68b5e61f0a5ec01e051b450",
"sig:ET\\Builder\\Packages\\GlobalData\\GlobalPreset::get_group_preset_default_attr": "427ee58858537cf02a9d6dbbf58aafd779f8424c9c840806b5d7af8f25fbbda5",
"sig:ET\\Builder\\Packages\\GlobalData\\GlobalPreset::prepare_data": "94f109150a8d6db57c309de00f4f4e369b4cd4a7e2a0682d5612dddeb65c8861",
"sig:ET\\Builder\\Packages\\GlobalData\\GlobalPreset::remove_preset_from_legacy_option": "11f8d1b50b8f113f96c9d8b2fb79454d45938a9d1613f42f5089e35767597aed",
"sig:ET\\Builder\\Packages\\GlobalData\\GlobalPreset::save_data": "7cfacdb2d6231f4baff0bea176081bf3237b5c3fb2a3f8e76fcd8b7d156d4375",
"sig:ET\\Builder\\Packages\\GlobalData\\GlobalPreset::validate_preset_count": "09765c551d2f4fb3d5841644f79aa304b7406d92aed6a1cc33821f80dc8b6592",
"sig:ET\\Builder\\Packages\\GlobalLayout\\GlobalLayout::is_global_layout_template": "6fe723bf98c5fa7580f9f52af29adf08a037520081ced8363063125270ae3384",
"sig:ET\\Builder\\Packages\\ModuleLibrary\\ModuleRegistration::get_all_core_modules_metadata": "dfbf01f6750e3fa6c309f1200effcfda539d75c3404732cd5df939a8bff399ae",
"sig:ET\\Builder\\Packages\\ModuleLibrary\\ModuleRegistration::get_default_attrs": "728c9cddae0184ffccaff5b22a392b7a4e9c5f078b5d12d4c9717aca6af70e69",
"sig:ET\\Builder\\Packages\\ModuleLibrary\\ModuleRegistration::get_module_settings": "f61b3693b0df17be1f068b8be424dcf56a67324372a92f576724cd564d992036",
"sig:ET\\Builder\\Packages\\Module\\Layout\\Components\\DynamicContent\\DynamicContentOptions::get_options": "6d3932cbf7921be4f353ab71d2bd7200c2bf500577b401b32e9ca31809f07489",
"sig:ET_Core_PageResource::remove_static_resources": "eaa39fd2c037c2d399578f71862da5c04b68d58a30657916482f95e94e5d8a96",
"sig:et_": "ae7b0ec701e388b03d29d972195cb727fe1c8fd75dba4657ad71f1a963a32966",
"sig:et_builder_enabled_for_post_type": "a6aed8043b2c07ef1082488fde706816cf0146847f5bac8acca79fb3fe5275fc",
"sig:et_builder_get_public_post_types": "ae7b0ec701e388b03d29d972195cb727fe1c8fd75dba4657ad71f1a963a32966",
"sig:et_pb_is_allowed": "b7561d91c3e696079a598474517118c3e9809cbf1f08b1321babe7af11d15799",
"sig:et_theme_builder_clear_wp_cache": "3ac3132305f3818c13df78416ae13e34c36af17a0ba9b66d652fe9babc6c051d",
"sig:et_theme_builder_get_flat_template_settings_options": "ae7b0ec701e388b03d29d972195cb727fe1c8fd75dba4657ad71f1a963a32966",
"sig:et_theme_builder_get_template": "a6aed8043b2c07ef1082488fde706816cf0146847f5bac8acca79fb3fe5275fc",
"sig:et_theme_builder_get_theme_builder_post_id": "b7561d91c3e696079a598474517118c3e9809cbf1f08b1321babe7af11d15799",
"sig:et_theme_builder_get_theme_builder_template_ids": "b7561d91c3e696079a598474517118c3e9809cbf1f08b1321babe7af11d15799",
"sig:et_theme_builder_insert_layout": "a6aed8043b2c07ef1082488fde706816cf0146847f5bac8acca79fb3fe5275fc",
"sig:et_theme_builder_store_template": "6c84ec472739a2dbb87bcf21777c4dc977fd2233142eacfefe60eba462a4c219",
"tb-api-save": "7ba452f4d73c9b676281ca6ddcfddd669502965db868bf478ef7804937e5bfc4",
"tb-load-setting-options": "2ea0a6925a1da5522489a417104b384b2bd77a16b25f4122b9503873a4c8a24e",
"tb-store-template": "3bf4ac6028ed99ed8ac3523c659392a5fce7818ef8a80ea226e01f5404eb441d",