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:
co-authored by
Claude Sonnet 5.5
parent
392fe7c569
commit
710444e88d
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user