Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
135 lines
4.9 KiB
PHP
135 lines
4.9 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace LIT\MCP\Integrations\Divi\Release;
|
||
|
||
use RuntimeException;
|
||
|
||
defined('ABSPATH') || exit;
|
||
|
||
/**
|
||
* 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';
|
||
|
||
/** 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;
|
||
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- local plugin file.
|
||
$raw = is_file($file) && is_readable($file) ? file_get_contents($file) : false;
|
||
$data = is_string($raw) ? json_decode($raw, true) : null;
|
||
if (! is_array($data)) {
|
||
return [];
|
||
}
|
||
|
||
$expected = [];
|
||
foreach ($data as $id => $hash) {
|
||
if (is_string($id) && is_string($hash)) {
|
||
$expected[$id] = $hash;
|
||
}
|
||
}
|
||
ksort($expected);
|
||
|
||
return $expected;
|
||
}
|
||
|
||
/**
|
||
* @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
|
||
{
|
||
ksort($hashes);
|
||
// phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode -- pure PHP, no WordPress.
|
||
$json = json_encode($hashes, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- dev fixture.
|
||
if (! is_string($json) || file_put_contents($file ?? self::FILE, $json . "\n") === false) {
|
||
throw new RuntimeException('fingerprints.json not written');
|
||
}
|
||
}
|
||
|
||
/** 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
|
||
{
|
||
$dir = dirname(__DIR__, 4) . '/tests/Fixtures/divi/reference';
|
||
|
||
return is_dir($dir) ? $dir : null;
|
||
}
|
||
|
||
public static function readSource(string $id, string $dir): ?string
|
||
{
|
||
$file = $dir . '/' . $id . '.txt';
|
||
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- local dev fixture.
|
||
$source = is_file($file) ? file_get_contents($file) : false;
|
||
|
||
return is_string($source) ? $source : null;
|
||
}
|
||
|
||
/** @throws RuntimeException */
|
||
public static function writeSource(string $id, string $source, string $dir): void
|
||
{
|
||
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- dev fixture.
|
||
if (file_put_contents($dir . '/' . $id . '.txt', $source . "\n") === false) {
|
||
throw new RuntimeException(sprintf('reference source %s not written', $id));
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Fingerprints of every mirror in the installed Divi.
|
||
*
|
||
* @return array<string, SourceFingerprint>
|
||
* @throws RuntimeException When a mirrored function cannot be read.
|
||
*/
|
||
public static function current(string $themeDir): array
|
||
{
|
||
$prints = [];
|
||
foreach (DiviAdapters::mirrors() as $mirror) {
|
||
$file = $themeDir . '/' . $mirror->file;
|
||
$prints[$mirror->id] = SourceFingerprint::of($file, $mirror->function, $mirror->class);
|
||
}
|
||
|
||
return $prints;
|
||
}
|
||
}
|