refactor(compat): Prüfbausteine nach src/Compatibility, Matrix und ManifestCheck generisch
Requirement, Call, ConstantRequirement, Mirror, Problem, FeatureStatus, CompatibilityMatrix, Manifest, ManifestCheck sowie Fingerprints und LineDiff liegen jetzt im divi-freien Kern. Neu: GlobalRequirement, CompatibilityMatrix::fromProblems(), ManifestCheck-Option signatures, DiviManifestCheck::create() und DiviAreas. Divi-Verhalten unverändert. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
9beb55e228
commit
b511ef6348
@@ -9,15 +9,15 @@ use LIT\MCP\Admin\AdminPostAction;
|
||||
use LIT\MCP\Admin\AdminTab;
|
||||
use LIT\MCP\Admin\SettingsPage;
|
||||
use LIT\MCP\ChangeLog\ChangeLog;
|
||||
use LIT\MCP\Compatibility\ReleaseState;
|
||||
use LIT\MCP\Integrations\Divi\Release\CheckReport;
|
||||
use LIT\MCP\Integrations\Divi\Release\CheckReportStore;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviCheckRoute;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviEnvironment;
|
||||
use LIT\MCP\Integrations\Divi\Release\FeatureOverview;
|
||||
use LIT\MCP\Integrations\Divi\Release\FeatureStatus;
|
||||
use LIT\MCP\Compatibility\FeatureStatus;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseResolver;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseState;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseStatus;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseStore;
|
||||
use LIT\MCP\Integrations\Divi\Release\WriteProbe;
|
||||
|
||||
@@ -5,17 +5,17 @@ declare(strict_types=1);
|
||||
namespace LIT\MCP\Integrations\Divi\Cli;
|
||||
|
||||
use Closure;
|
||||
use LIT\MCP\Compatibility\LineDiff;
|
||||
use LIT\MCP\Compatibility\SourceFingerprint;
|
||||
use LIT\MCP\Integrations\Divi\Release\CheckReport;
|
||||
use LIT\MCP\Integrations\Divi\Release\CheckReportStore;
|
||||
use LIT\MCP\Integrations\Divi\Release\CheckStep;
|
||||
use LIT\MCP\Integrations\Divi\Release\CompatibilityCheck;
|
||||
use LIT\MCP\Integrations\Divi\Release\CompatibilityMatrix;
|
||||
use LIT\MCP\Compatibility\CompatibilityMatrix;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviAdapters;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviEnvironment;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviReference;
|
||||
use LIT\MCP\Integrations\Divi\Release\LineDiff;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
|
||||
use LIT\MCP\Integrations\Divi\Release\SourceFingerprint;
|
||||
use RuntimeException;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
@@ -6,10 +6,11 @@ namespace LIT\MCP\Integrations\Divi\Design;
|
||||
|
||||
use ET\Builder\Packages\GlobalData\GlobalData;
|
||||
use ET\Builder\Packages\GlobalData\GlobalPreset;
|
||||
use LIT\MCP\Integrations\Divi\Release\Call;
|
||||
use LIT\MCP\Compatibility\Call;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviAdapter;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviAreas;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use LIT\MCP\Integrations\Divi\Release\Manifest;
|
||||
use LIT\MCP\Compatibility\Manifest;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
@@ -29,7 +30,7 @@ class DesignSystemReader implements DiviAdapter
|
||||
public static function manifest(): Manifest
|
||||
{
|
||||
return new Manifest(
|
||||
Manifest::DESIGN,
|
||||
DiviAreas::DESIGN,
|
||||
calls: [
|
||||
new Call(GlobalData::class . '::get_global_colors', 0),
|
||||
new Call(GlobalData::class . '::get_global_variables', 0),
|
||||
|
||||
@@ -8,12 +8,13 @@ use ET\Builder\Framework\UserRole\UserRole;
|
||||
use ET\Builder\Packages\Conversion\Conversion;
|
||||
use ET\Builder\Packages\GlobalData\GlobalPreset;
|
||||
use ET\Builder\Packages\ModuleLibrary\ModuleRegistration;
|
||||
use LIT\MCP\Integrations\Divi\Release\Call;
|
||||
use LIT\MCP\Compatibility\Call;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviAdapter;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviAreas;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviLocked;
|
||||
use LIT\MCP\Integrations\Divi\Release\Manifest;
|
||||
use LIT\MCP\Integrations\Divi\Release\Mirror;
|
||||
use LIT\MCP\Compatibility\Manifest;
|
||||
use LIT\MCP\Compatibility\Mirror;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
|
||||
use RuntimeException;
|
||||
use WP_Error;
|
||||
@@ -43,7 +44,7 @@ class DesignSystemWriter implements DiviAdapter
|
||||
public static function manifest(): Manifest
|
||||
{
|
||||
return new Manifest(
|
||||
Manifest::DESIGN,
|
||||
DiviAreas::DESIGN,
|
||||
calls: [
|
||||
// Read by the preset tools as well as by the design system tool.
|
||||
new Call(GlobalPreset::class . '::get_data', 0, [DiviFeature::DesignRead, DiviFeature::PresetWrite]),
|
||||
|
||||
@@ -4,10 +4,11 @@ declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Design;
|
||||
|
||||
use LIT\MCP\Integrations\Divi\Release\Call;
|
||||
use LIT\MCP\Compatibility\Call;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviAdapter;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviAreas;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use LIT\MCP\Integrations\Divi\Release\Manifest;
|
||||
use LIT\MCP\Compatibility\Manifest;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
@@ -26,7 +27,7 @@ class FontCatalog implements DiviAdapter
|
||||
public static function manifest(): Manifest
|
||||
{
|
||||
return new Manifest(
|
||||
Manifest::DESIGN,
|
||||
DiviAreas::DESIGN,
|
||||
calls: [
|
||||
new Call('et_builder_get_fonts', 0),
|
||||
new Call('et_builder_get_custom_fonts', 0),
|
||||
|
||||
@@ -6,12 +6,13 @@ namespace LIT\MCP\Integrations\Divi\Design;
|
||||
|
||||
use ET\Builder\VisualBuilder\Fonts\FontsUtility;
|
||||
use ET\Builder\VisualBuilder\Hooks\HooksRegistration;
|
||||
use LIT\MCP\Integrations\Divi\Release\Call;
|
||||
use LIT\MCP\Compatibility\Call;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviAdapter;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviAreas;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviLocked;
|
||||
use LIT\MCP\Integrations\Divi\Release\Manifest;
|
||||
use LIT\MCP\Integrations\Divi\Release\Mirror;
|
||||
use LIT\MCP\Compatibility\Manifest;
|
||||
use LIT\MCP\Compatibility\Mirror;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
|
||||
use RuntimeException;
|
||||
|
||||
@@ -35,7 +36,7 @@ class FontInstaller implements DiviAdapter
|
||||
public static function manifest(): Manifest
|
||||
{
|
||||
return new Manifest(
|
||||
Manifest::DESIGN,
|
||||
DiviAreas::DESIGN,
|
||||
calls: [
|
||||
new Call(FontsUtility::class . '::mime_types_font', 0),
|
||||
new Call(HooksRegistration::class . '::check_filetype_and_ext_font', 3),
|
||||
|
||||
@@ -5,11 +5,12 @@ declare(strict_types=1);
|
||||
namespace LIT\MCP\Integrations\Divi\Design;
|
||||
|
||||
use ET\Builder\Packages\GlobalData\GlobalData;
|
||||
use LIT\MCP\Integrations\Divi\Release\Call;
|
||||
use LIT\MCP\Compatibility\Call;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviAdapter;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviAreas;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviLocked;
|
||||
use LIT\MCP\Integrations\Divi\Release\Manifest;
|
||||
use LIT\MCP\Compatibility\Manifest;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
|
||||
use RuntimeException;
|
||||
|
||||
@@ -39,7 +40,7 @@ class GlobalColorsAdapter implements DiviAdapter
|
||||
public static function manifest(): Manifest
|
||||
{
|
||||
return new Manifest(
|
||||
Manifest::DESIGN,
|
||||
DiviAreas::DESIGN,
|
||||
calls: [
|
||||
new Call(GlobalData::class . '::get_global_colors', 0),
|
||||
new Call(GlobalData::class . '::sanitize_global_colors_data', 1),
|
||||
|
||||
@@ -5,11 +5,12 @@ declare(strict_types=1);
|
||||
namespace LIT\MCP\Integrations\Divi\Design;
|
||||
|
||||
use ET\Builder\Packages\GlobalData\GlobalData;
|
||||
use LIT\MCP\Integrations\Divi\Release\Call;
|
||||
use LIT\MCP\Compatibility\Call;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviAdapter;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviAreas;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviLocked;
|
||||
use LIT\MCP\Integrations\Divi\Release\Manifest;
|
||||
use LIT\MCP\Compatibility\Manifest;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
|
||||
use RuntimeException;
|
||||
use WP_Error;
|
||||
@@ -28,7 +29,7 @@ class GlobalVariablesAdapter implements DiviAdapter
|
||||
public static function manifest(): Manifest
|
||||
{
|
||||
return new Manifest(
|
||||
Manifest::DESIGN,
|
||||
DiviAreas::DESIGN,
|
||||
calls: [
|
||||
new Call(GlobalData::class . '::get_global_variables', 0),
|
||||
new Call(GlobalData::class . '::set_global_variables', 2),
|
||||
|
||||
@@ -5,12 +5,13 @@ declare(strict_types=1);
|
||||
namespace LIT\MCP\Integrations\Divi\Design;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use LIT\MCP\Integrations\Divi\Release\Call;
|
||||
use LIT\MCP\Compatibility\Call;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviAdapter;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviAreas;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviLocked;
|
||||
use LIT\MCP\Integrations\Divi\Release\Manifest;
|
||||
use LIT\MCP\Integrations\Divi\Release\Mirror;
|
||||
use LIT\MCP\Compatibility\Manifest;
|
||||
use LIT\MCP\Compatibility\Mirror;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
|
||||
use RuntimeException;
|
||||
|
||||
@@ -61,7 +62,7 @@ class ThemeOptionsAdapter implements DiviAdapter
|
||||
public static function manifest(): Manifest
|
||||
{
|
||||
return new Manifest(
|
||||
Manifest::DESIGN,
|
||||
DiviAreas::DESIGN,
|
||||
calls: [
|
||||
new Call('et_sanitize_font_choices', 1),
|
||||
new Call('et_sanitize_global_font_weight', 1),
|
||||
|
||||
@@ -4,7 +4,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi;
|
||||
|
||||
use LIT\MCP\Integrations\Divi\Release\ManifestCheck;
|
||||
use LIT\MCP\Compatibility\ManifestCheck;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviManifestCheck;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
@@ -29,7 +30,7 @@ final class DiviCompatibility
|
||||
/** @return array<string, callable(): bool> */
|
||||
public static function defaults(?ManifestCheck $check = null): array
|
||||
{
|
||||
$check ??= new ManifestCheck();
|
||||
$check ??= DiviManifestCheck::create();
|
||||
|
||||
return array_merge($check->codeChecks(), $check->registrationChecks(), $check->mirrorChecks());
|
||||
}
|
||||
|
||||
@@ -5,12 +5,13 @@ declare(strict_types=1);
|
||||
namespace LIT\MCP\Integrations\Divi\Library;
|
||||
|
||||
use LIT\MCP\Abilities\WriteFailed;
|
||||
use LIT\MCP\Integrations\Divi\Release\Call;
|
||||
use LIT\MCP\Compatibility\Call;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviAdapter;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviAreas;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviLocked;
|
||||
use LIT\MCP\Integrations\Divi\Release\Manifest;
|
||||
use LIT\MCP\Integrations\Divi\Release\Mirror;
|
||||
use LIT\MCP\Compatibility\Manifest;
|
||||
use LIT\MCP\Compatibility\Mirror;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
|
||||
use LIT\MCP\Integrations\WordPress\Content\NewDraft;
|
||||
use RuntimeException;
|
||||
@@ -55,7 +56,7 @@ class DiviLibraryStore implements DiviAdapter
|
||||
public static function manifest(): Manifest
|
||||
{
|
||||
return new Manifest(
|
||||
Manifest::LIBRARY,
|
||||
DiviAreas::LIBRARY,
|
||||
calls: [
|
||||
// isGlobal() serves listing and describing as well as the global edits.
|
||||
new Call(
|
||||
|
||||
@@ -6,10 +6,11 @@ namespace LIT\MCP\Integrations\Divi\Library;
|
||||
|
||||
use Closure;
|
||||
use LIT\MCP\Abilities\Errors;
|
||||
use LIT\MCP\Integrations\Divi\Release\Call;
|
||||
use LIT\MCP\Compatibility\Call;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviAdapter;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviAreas;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use LIT\MCP\Integrations\Divi\Release\Manifest;
|
||||
use LIT\MCP\Compatibility\Manifest;
|
||||
use WP_Error;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
@@ -35,7 +36,7 @@ class DiviRoles implements DiviAdapter
|
||||
public static function manifest(): Manifest
|
||||
{
|
||||
return new Manifest(
|
||||
Manifest::LIBRARY,
|
||||
DiviAreas::LIBRARY,
|
||||
calls: [new Call('et_pb_is_allowed', 1)],
|
||||
features: [DiviFeature::Roles]
|
||||
);
|
||||
|
||||
@@ -4,12 +4,13 @@ declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Pages;
|
||||
|
||||
use LIT\MCP\Integrations\Divi\Release\Call;
|
||||
use LIT\MCP\Compatibility\Call;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviAdapter;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviAreas;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviLocked;
|
||||
use LIT\MCP\Integrations\Divi\Release\Manifest;
|
||||
use LIT\MCP\Integrations\Divi\Release\Mirror;
|
||||
use LIT\MCP\Compatibility\Manifest;
|
||||
use LIT\MCP\Compatibility\Mirror;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
|
||||
use RuntimeException;
|
||||
use WP_Post;
|
||||
@@ -35,7 +36,7 @@ final class DiviPages implements DiviAdapter
|
||||
public static function manifest(): Manifest
|
||||
{
|
||||
return new Manifest(
|
||||
Manifest::PAGES,
|
||||
DiviAreas::PAGES,
|
||||
calls: [
|
||||
// Also called by DiviThemeBuilder::clearSiteCaches() – a break locks both features.
|
||||
new Call(
|
||||
|
||||
@@ -8,10 +8,11 @@ use Closure;
|
||||
use LIT\MCP\Integrations\Divi\Compose\Placeholders;
|
||||
use LIT\MCP\Integrations\Divi\Compose\DynamicContentRules;
|
||||
use LIT\MCP\Integrations\Divi\Compose\LoopScope;
|
||||
use LIT\MCP\Integrations\Divi\Release\Call;
|
||||
use LIT\MCP\Compatibility\Call;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviAdapter;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviAreas;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use LIT\MCP\Integrations\Divi\Release\Manifest;
|
||||
use LIT\MCP\Compatibility\Manifest;
|
||||
use LIT\MCP\Log;
|
||||
use Throwable;
|
||||
|
||||
@@ -56,7 +57,7 @@ class DynamicContentMap implements DiviAdapter
|
||||
public static function manifest(): Manifest
|
||||
{
|
||||
return new Manifest(
|
||||
Manifest::CATALOG,
|
||||
DiviAreas::CATALOG,
|
||||
calls: [new Call(self::REGISTRY . '::get_options', 2)],
|
||||
features: [DiviFeature::DynamicSources]
|
||||
);
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Release;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* One Divi function or static method an adapter calls (spec §4.1), with the number of arguments the plugin passes.
|
||||
* $args null: only its existence matters (a function Divi itself needs, e.g. et_() inside store_template).
|
||||
*/
|
||||
final class Call implements Requirement
|
||||
{
|
||||
/**
|
||||
* @param string $symbol "function_name" or "Fully\Qualified\Class::method".
|
||||
* @param list<DiviFeature> $features [] = the default of the manifest.
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly string $symbol,
|
||||
public readonly ?int $args = null,
|
||||
private readonly array $features = []
|
||||
) {
|
||||
}
|
||||
|
||||
public function className(): ?string
|
||||
{
|
||||
return str_contains($this->symbol, '::') ? ltrim(explode('::', $this->symbol, 2)[0], '\\') : null;
|
||||
}
|
||||
|
||||
public function member(): string
|
||||
{
|
||||
return str_contains($this->symbol, '::') ? explode('::', $this->symbol, 2)[1] : $this->symbol;
|
||||
}
|
||||
|
||||
public function features(): array
|
||||
{
|
||||
return $this->features;
|
||||
}
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
$class = $this->className();
|
||||
|
||||
return ($class === null ? '' : SourceFingerprint::shortName($class) . '::') . $this->member() . '()';
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace LIT\MCP\Integrations\Divi\Release;
|
||||
|
||||
use Closure;
|
||||
use LIT\MCP\Compatibility\ManifestCheck;
|
||||
use LIT\MCP\Integrations\Divi\Schema\DiviModules;
|
||||
use LIT\MCP\Integrations\Divi\Schema\ShortformCheck;
|
||||
use LIT\MCP\Integrations\WordPress\Content\ContentComposer;
|
||||
@@ -40,7 +41,7 @@ class CompatibilityCheck
|
||||
public static function create(ContentComposer $composer): self
|
||||
{
|
||||
return new self(
|
||||
new ManifestCheck(),
|
||||
DiviManifestCheck::create(),
|
||||
static fn (): array => DiviModules::isAvailable()
|
||||
? ShortformCheck::problems((new DiviModules())->catalog())
|
||||
: ['Divi 5 ist in dieser Anfrage nicht geladen.'],
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Release;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* The release state of every Divi feature (E1). A feature the constructor did not get is incompatible (fail
|
||||
* closed), so of() always answers. The overall state of the matrix is its worst feature state.
|
||||
*/
|
||||
final class CompatibilityMatrix
|
||||
{
|
||||
public const MISSING = 'Für diese Fähigkeit liegt kein Prüfergebnis vor.';
|
||||
|
||||
/** @var array<string, FeatureStatus> keyed by DiviFeature value, in DiviFeature::cases() order */
|
||||
private array $features = [];
|
||||
|
||||
/** @param array<string, FeatureStatus> $features keyed by DiviFeature value */
|
||||
public function __construct(array $features)
|
||||
{
|
||||
foreach (DiviFeature::cases() as $feature) {
|
||||
$status = $features[$feature->value] ?? null;
|
||||
$this->features[$feature->value] = $status instanceof FeatureStatus && $status->feature === $feature
|
||||
? $status
|
||||
: new FeatureStatus($feature, ReleaseState::Incompatible, [self::MISSING]);
|
||||
}
|
||||
}
|
||||
|
||||
public static function allReleased(string $source = ReleaseStatus::SOURCE_CODE): self
|
||||
{
|
||||
return self::uniform(ReleaseState::Released, [], $source);
|
||||
}
|
||||
|
||||
public static function allIncompatible(string $problem): self
|
||||
{
|
||||
return self::uniform(ReleaseState::Incompatible, [$problem]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Every feature in $state – the matrix of a status built without one (ReleaseStatus' constructor).
|
||||
*
|
||||
* @param list<string> $problems
|
||||
*/
|
||||
public static function uniform(ReleaseState $state, array $problems = [], ?string $source = null): self
|
||||
{
|
||||
$released = $state === ReleaseState::Released;
|
||||
$features = [];
|
||||
foreach (DiviFeature::cases() as $feature) {
|
||||
$features[$feature->value] = new FeatureStatus(
|
||||
$feature,
|
||||
$state,
|
||||
$released ? [] : $problems,
|
||||
$released ? ($source ?? ReleaseStatus::SOURCE_CODE) : null
|
||||
);
|
||||
}
|
||||
|
||||
return new self($features);
|
||||
}
|
||||
|
||||
public function of(DiviFeature $feature): FeatureStatus
|
||||
{
|
||||
return $this->features[$feature->value];
|
||||
}
|
||||
|
||||
/** @param list<DiviFeature> $features */
|
||||
public function usable(array $features): bool
|
||||
{
|
||||
foreach ($features as $feature) {
|
||||
if (! $this->of($feature)->usable()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* The statuses of those of $features that are not usable – each once, in the order of $features. The one answer
|
||||
* to "what locks this call" for the gate (DiviFeatureGate), the adapters (ReleaseGate::assertFeatures()) and the
|
||||
* refusal (ReleaseStatus::refusalFor()).
|
||||
*
|
||||
* @param list<DiviFeature> $features
|
||||
* @return list<FeatureStatus>
|
||||
*/
|
||||
public function locked(array $features): array
|
||||
{
|
||||
$locked = [];
|
||||
foreach ($features as $feature) {
|
||||
$status = $this->of($feature);
|
||||
if (! $status->usable()) {
|
||||
$locked[$feature->value] = $status;
|
||||
}
|
||||
}
|
||||
|
||||
return array_values($locked);
|
||||
}
|
||||
|
||||
/** @return list<FeatureStatus> */
|
||||
public function unusable(): array
|
||||
{
|
||||
return array_values(array_filter(
|
||||
$this->features,
|
||||
static fn (FeatureStatus $status): bool => ! $status->usable()
|
||||
));
|
||||
}
|
||||
|
||||
/** Incompatible before untested before released. */
|
||||
public function state(): ReleaseState
|
||||
{
|
||||
$states = array_map(static fn (FeatureStatus $status): ReleaseState => $status->state, $this->features);
|
||||
|
||||
return match (true) {
|
||||
in_array(ReleaseState::Incompatible, $states, true) => ReleaseState::Incompatible,
|
||||
in_array(ReleaseState::Untested, $states, true) => ReleaseState::Untested,
|
||||
default => ReleaseState::Released,
|
||||
};
|
||||
}
|
||||
|
||||
/** The common source of a fully released matrix; null when not all released or the sources differ. */
|
||||
public function source(): ?string
|
||||
{
|
||||
$sources = array_unique(array_map(
|
||||
static fn (FeatureStatus $status): string => (string) $status->source,
|
||||
$this->features
|
||||
));
|
||||
|
||||
return $this->state() === ReleaseState::Released && count($sources) === 1 ? (string) reset($sources) : null;
|
||||
}
|
||||
|
||||
/** @return list<string> Every problem label once, in feature order. */
|
||||
public function problems(): array
|
||||
{
|
||||
$problems = [];
|
||||
foreach ($this->features as $status) {
|
||||
$problems = array_merge($problems, $status->problems);
|
||||
}
|
||||
|
||||
return array_values(array_unique($problems));
|
||||
}
|
||||
|
||||
/** @return list<array{id: string, label: string, state: string, problems: list<string>, source: ?string}> */
|
||||
public function toArray(): array
|
||||
{
|
||||
return array_values(array_map(static fn (FeatureStatus $status): array => $status->toArray(), $this->features));
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Release;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/** A Divi constant an adapter reads (spec §4.1); it must be defined. */
|
||||
final class ConstantRequirement implements Requirement
|
||||
{
|
||||
/** @param list<DiviFeature> $features [] = the default of the manifest. */
|
||||
public function __construct(public readonly string $name, private readonly array $features = [])
|
||||
{
|
||||
}
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return 'Konstante ' . $this->name;
|
||||
}
|
||||
|
||||
public function features(): array
|
||||
{
|
||||
return $this->features;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Release;
|
||||
|
||||
use LIT\MCP\Compatibility\Manifest;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/** A class that may touch Divi (spec §3); it lists every Divi dependency in its manifest. */
|
||||
|
||||
@@ -4,6 +4,9 @@ declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Release;
|
||||
|
||||
use LIT\MCP\Compatibility\Call;
|
||||
use LIT\MCP\Compatibility\Manifest;
|
||||
use LIT\MCP\Compatibility\Mirror;
|
||||
use LIT\MCP\Integrations\Divi\Design\DesignSystemReader;
|
||||
use LIT\MCP\Integrations\Divi\Design\DesignSystemWriter;
|
||||
use LIT\MCP\Integrations\Divi\Design\FontCatalog;
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Release;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/** The five Divi areas of spec §3. Each value is the prefix of the labels in the admin check. */
|
||||
final class DiviAreas
|
||||
{
|
||||
public const PAGES = 'Seiten';
|
||||
public const LIBRARY = 'Bibliothek';
|
||||
public const THEME_BUILDER = 'Theme Builder';
|
||||
public const DESIGN = 'Designsystem';
|
||||
public const CATALOG = 'Katalog';
|
||||
}
|
||||
@@ -8,6 +8,7 @@ use LIT\MCP\Abilities\Ability;
|
||||
use LIT\MCP\Abilities\AbilityGuard;
|
||||
use LIT\MCP\Abilities\Errors;
|
||||
use LIT\MCP\ChangeLog\ChangeLog;
|
||||
use LIT\MCP\Compatibility\ReleaseState;
|
||||
use WP_Error;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Release;
|
||||
|
||||
use LIT\MCP\Compatibility\ManifestCheck;
|
||||
use LIT\MCP\Compatibility\Mirror;
|
||||
use LIT\MCP\Compatibility\SourceFingerprint;
|
||||
use LIT\MCP\Integrations\Divi\Schema\DiviModules;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/** The ManifestCheck of Divi: its adapter manifests, signature references, theme sources and block registry. */
|
||||
final class DiviManifestCheck
|
||||
{
|
||||
public static function create(): ManifestCheck
|
||||
{
|
||||
return new ManifestCheck(
|
||||
DiviAdapters::manifests(),
|
||||
DiviReference::expected(),
|
||||
self::installed(...),
|
||||
DiviModules::knowsBlock(...)
|
||||
);
|
||||
}
|
||||
|
||||
private static function installed(Mirror $mirror): ?string
|
||||
{
|
||||
$dir = DiviEnvironment::themeDir();
|
||||
|
||||
return $dir === null
|
||||
? null
|
||||
: SourceFingerprint::of($dir . '/' . $mirror->file, $mirror->function, $mirror->class)->hash;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,9 @@ declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Release;
|
||||
|
||||
use LIT\MCP\Compatibility\Call;
|
||||
use LIT\MCP\Compatibility\SignatureFingerprint;
|
||||
use LIT\MCP\Compatibility\SourceFingerprint;
|
||||
use RuntimeException;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
@@ -18,8 +21,7 @@ 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:';
|
||||
public const SIGNATURE_PREFIX = Call::SIGNATURE_PREFIX;
|
||||
|
||||
/**
|
||||
* @return array<string, string> Mirror ID or "sig:<symbol>" => sha256, sorted by key; [] when the file is
|
||||
@@ -61,12 +63,6 @@ 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).
|
||||
*
|
||||
@@ -82,7 +78,7 @@ final class DiviReference
|
||||
if ($hash === null) {
|
||||
throw new RuntimeException(sprintf('signature of %s: symbol is not loaded', $call->label()));
|
||||
}
|
||||
$signatures[self::signatureKey($call)] = $hash;
|
||||
$signatures[$call->signatureKey()] = $hash;
|
||||
}
|
||||
ksort($signatures);
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace LIT\MCP\Integrations\Divi\Release;
|
||||
|
||||
use Closure;
|
||||
use LIT\MCP\Abilities\Ability;
|
||||
use LIT\MCP\Compatibility\CompatibilityMatrix;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Release;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/** The release state of one Divi feature (E1): released (with its source), untested or incompatible. */
|
||||
final class FeatureStatus
|
||||
{
|
||||
/**
|
||||
* @param list<string> $problems Labels of the problems behind the state (Problem::$label).
|
||||
* @param string|null $source For a released feature: ReleaseStatus::SOURCE_CODE, SOURCE_DATABASE or
|
||||
* SOURCE_AUTO; null otherwise.
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly DiviFeature $feature,
|
||||
public readonly ReleaseState $state,
|
||||
public readonly array $problems = [],
|
||||
public readonly ?string $source = null
|
||||
) {
|
||||
}
|
||||
|
||||
public function usable(): bool
|
||||
{
|
||||
return $this->state === ReleaseState::Released;
|
||||
}
|
||||
|
||||
/** @return array{id: string, label: string, state: string, problems: list<string>} For refusals (D6). */
|
||||
public function summary(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->feature->value,
|
||||
'label' => $this->feature->label(),
|
||||
'state' => $this->state->value,
|
||||
'problems' => $this->problems,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{id: string, label: string, state: string, problems: list<string>, source: ?string}
|
||||
* For lit-site-status and the admin tab.
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return $this->summary() + ['source' => $this->source];
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Release;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/** A small line diff (longest common subsequence) for wp lit-mcp divi-check --diff. */
|
||||
final class LineDiff
|
||||
{
|
||||
/**
|
||||
* @param list<string> $old
|
||||
* @param list<string> $new
|
||||
* @return list<string> " kept", "- removed", "+ added"; "…" between changes more than 2 × $context lines apart.
|
||||
*/
|
||||
public static function lines(array $old, array $new, int $context = 2): array
|
||||
{
|
||||
$operations = self::operations($old, $new);
|
||||
$keep = [];
|
||||
foreach ($operations as $i => [$op]) {
|
||||
if ($op === ' ') {
|
||||
continue;
|
||||
}
|
||||
for ($j = max(0, $i - $context); $j <= min(count($operations) - 1, $i + $context); ++$j) {
|
||||
$keep[$j] = true;
|
||||
}
|
||||
}
|
||||
|
||||
$lines = [];
|
||||
$last = null;
|
||||
foreach ($operations as $i => [$op, $line]) {
|
||||
if (! isset($keep[$i])) {
|
||||
continue;
|
||||
}
|
||||
if ($last !== null && $i > $last + 1) {
|
||||
$lines[] = '…';
|
||||
}
|
||||
$lines[] = $op . ' ' . $line;
|
||||
$last = $i;
|
||||
}
|
||||
|
||||
return $lines;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $a
|
||||
* @param list<string> $b
|
||||
* @return list<array{0: string, 1: string}>
|
||||
*/
|
||||
private static function operations(array $a, array $b): array
|
||||
{
|
||||
$n = count($a);
|
||||
$m = count($b);
|
||||
$lcs = array_fill(0, $n + 1, array_fill(0, $m + 1, 0));
|
||||
for ($i = $n - 1; $i >= 0; --$i) {
|
||||
for ($j = $m - 1; $j >= 0; --$j) {
|
||||
$lcs[$i][$j] = $a[$i] === $b[$j] ? $lcs[$i + 1][$j + 1] + 1 : max($lcs[$i + 1][$j], $lcs[$i][$j + 1]);
|
||||
}
|
||||
}
|
||||
|
||||
$operations = [];
|
||||
$i = 0;
|
||||
$j = 0;
|
||||
while ($i < $n && $j < $m) {
|
||||
if ($a[$i] === $b[$j]) {
|
||||
$operations[] = [' ', $a[$i]];
|
||||
++$i;
|
||||
++$j;
|
||||
} elseif ($lcs[$i + 1][$j] >= $lcs[$i][$j + 1]) {
|
||||
$operations[] = ['-', $a[$i++]];
|
||||
} else {
|
||||
$operations[] = ['+', $b[$j++]];
|
||||
}
|
||||
}
|
||||
while ($i < $n) {
|
||||
$operations[] = ['-', $a[$i++]];
|
||||
}
|
||||
while ($j < $m) {
|
||||
$operations[] = ['+', $b[$j++]];
|
||||
}
|
||||
|
||||
return $operations;
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Release;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* The Divi dependencies of one adapter (spec §4.1) – the single source for DiviCompatibility, the release gate,
|
||||
* the admin check and DiviBoundaryTest. Pure data, no WordPress calls.
|
||||
*/
|
||||
final class Manifest
|
||||
{
|
||||
public const PAGES = 'Seiten';
|
||||
public const LIBRARY = 'Bibliothek';
|
||||
public const THEME_BUILDER = 'Theme Builder';
|
||||
public const DESIGN = 'Designsystem';
|
||||
public const CATALOG = 'Katalog';
|
||||
|
||||
/** @var list<ConstantRequirement> */
|
||||
public readonly array $constants;
|
||||
|
||||
/**
|
||||
* @param string $area One of the constants above (the five areas of spec §3).
|
||||
* @param list<Call> $calls Divi functions and static methods the adapter calls.
|
||||
* @param list<string|ConstantRequirement> $constants Divi constants it reads; a plain name stands for a
|
||||
* ConstantRequirement without features of its own.
|
||||
* @param list<string> $postTypes Divi post types it needs registered.
|
||||
* @param list<string> $taxonomies Divi taxonomies it needs registered.
|
||||
* @param list<string> $blockTypes Divi block types it needs (registered or in Divi's
|
||||
* module metadata).
|
||||
* @param list<string> $keys Other Divi names it uses as strings: meta keys, options,
|
||||
* internal post types that need no registration check,
|
||||
* query arguments.
|
||||
* @param list<Mirror> $mirrors Divi functions whose storage logic it repeats.
|
||||
* @param list<DiviFeature> $features The features of every call, mirror and constant that names
|
||||
* none itself (featuresOf()).
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly string $area,
|
||||
public readonly array $calls = [],
|
||||
array $constants = [],
|
||||
public readonly array $postTypes = [],
|
||||
public readonly array $taxonomies = [],
|
||||
public readonly array $blockTypes = [],
|
||||
public readonly array $keys = [],
|
||||
public readonly array $mirrors = [],
|
||||
public readonly array $features = []
|
||||
) {
|
||||
$this->constants = array_map(
|
||||
static fn (string|ConstantRequirement $constant): ConstantRequirement => is_string($constant)
|
||||
? new ConstantRequirement($constant)
|
||||
: $constant,
|
||||
array_values($constants)
|
||||
);
|
||||
}
|
||||
|
||||
/** @return list<Requirement> Calls, constants and mirrors – everything the release gate checks as code. */
|
||||
public function requirements(): array
|
||||
{
|
||||
return [...$this->calls, ...$this->constants, ...$this->mirrors];
|
||||
}
|
||||
|
||||
/** @return list<DiviFeature> The features $requirement names itself, else this manifest's default. */
|
||||
public function featuresOf(Requirement $requirement): array
|
||||
{
|
||||
$own = $requirement->features();
|
||||
|
||||
return $own !== [] ? $own : $this->features;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string> Every Divi name the adapter's code may contain (DiviBoundaryTest): called functions and
|
||||
* classes, constants, post types, taxonomies, keys and the names of mirrored functions.
|
||||
*/
|
||||
public function names(): array
|
||||
{
|
||||
$names = [];
|
||||
foreach ($this->calls as $call) {
|
||||
$names[] = $call->className() ?? $call->member();
|
||||
}
|
||||
$constants = array_map(static fn (ConstantRequirement $constant): string => $constant->name, $this->constants);
|
||||
$mirrored = array_map(static fn (Mirror $mirror): string => $mirror->function, $this->mirrors);
|
||||
|
||||
return array_values(array_unique(array_merge(
|
||||
$names,
|
||||
$constants,
|
||||
$this->postTypes,
|
||||
$this->taxonomies,
|
||||
$this->keys,
|
||||
$mirrored
|
||||
)));
|
||||
}
|
||||
}
|
||||
@@ -1,256 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Release;
|
||||
|
||||
use Closure;
|
||||
use LIT\MCP\Integrations\Divi\Schema\DiviModules;
|
||||
use ReflectionFunction;
|
||||
use ReflectionMethod;
|
||||
use Throwable;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* The checks of all adapter manifests (spec §4.3), as label => check for reports, and as problems with their Divi
|
||||
* features for the release matrix (problems(), E1). Code checks (calls with the argument count the adapter passes
|
||||
* and their signature, constants) and mirror checks decide the release gate; registration checks (post types,
|
||||
* taxonomies, block types) depend on the request context (stage3 finding 6) and only show in reports.
|
||||
*
|
||||
* Not final so the resolver can be tested with a mock.
|
||||
*/
|
||||
class ManifestCheck
|
||||
{
|
||||
/** @var array<string, Manifest> */
|
||||
private array $manifests;
|
||||
|
||||
/** @var Closure(Mirror): ?string */
|
||||
private Closure $current;
|
||||
|
||||
/** @var Closure(string): bool */
|
||||
private Closure $knowsBlock;
|
||||
|
||||
/**
|
||||
* @param array<string, Manifest>|null $manifests null = DiviAdapters::manifests().
|
||||
* @param array<string, string>|null $expected Mirror ID or "sig:<symbol>" => sha256;
|
||||
* null = DiviReference::expected().
|
||||
* @param (Closure(Mirror): ?string)|null $current Hash of the installed mirror, null = unreadable;
|
||||
* null = SourceFingerprint in the Divi theme directory.
|
||||
* @param (Closure(string): bool)|null $knowsBlock null = DiviModules::knowsBlock().
|
||||
*/
|
||||
public function __construct(
|
||||
?array $manifests = null,
|
||||
private ?array $expected = null,
|
||||
?Closure $current = null,
|
||||
?Closure $knowsBlock = null
|
||||
) {
|
||||
$this->manifests = $manifests ?? DiviAdapters::manifests();
|
||||
$this->current = $current ?? self::installed(...);
|
||||
$this->knowsBlock = $knowsBlock ?? DiviModules::knowsBlock(...);
|
||||
}
|
||||
|
||||
/** @return array<string, Closure(): bool> */
|
||||
public function codeChecks(): array
|
||||
{
|
||||
$checks = [];
|
||||
foreach ($this->manifests as $manifest) {
|
||||
foreach ($manifest->calls as $call) {
|
||||
$checks[$manifest->area . ': ' . $call->label()] = static fn (): bool => self::accepts($call);
|
||||
}
|
||||
foreach ($manifest->constants as $constant) {
|
||||
$checks[$manifest->area . ': ' . $constant->label()] =
|
||||
static fn (): bool => defined($constant->name);
|
||||
}
|
||||
}
|
||||
|
||||
return $checks;
|
||||
}
|
||||
|
||||
/** @return array<string, Closure(): bool> */
|
||||
public function mirrorChecks(): array
|
||||
{
|
||||
$checks = [];
|
||||
foreach ($this->manifests as $manifest) {
|
||||
foreach ($manifest->mirrors as $mirror) {
|
||||
$checks[self::mirrorLabel($manifest, $mirror)] = fn (): bool => $this->matches($mirror);
|
||||
}
|
||||
}
|
||||
|
||||
return $checks;
|
||||
}
|
||||
|
||||
/** @return array<string, Closure(): bool> */
|
||||
public function registrationChecks(): array
|
||||
{
|
||||
$checks = [];
|
||||
foreach ($this->manifests as $manifest) {
|
||||
foreach ($manifest->postTypes as $type) {
|
||||
$checks[$manifest->area . ': Post-Typ ' . $type] = static fn (): bool => post_type_exists($type);
|
||||
}
|
||||
foreach ($manifest->taxonomies as $taxonomy) {
|
||||
$checks[$manifest->area . ': Taxonomie ' . $taxonomy] =
|
||||
static fn (): bool => taxonomy_exists($taxonomy);
|
||||
}
|
||||
foreach ($manifest->blockTypes as $block) {
|
||||
$checks[$manifest->area . ': Block-Typ ' . $block] = fn (): bool => ($this->knowsBlock)($block);
|
||||
}
|
||||
}
|
||||
|
||||
return $checks;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, callable(): bool> $checks
|
||||
* @return list<string> Labels of the checks that failed or threw.
|
||||
*/
|
||||
public static function failed(array $checks): array
|
||||
{
|
||||
$failed = [];
|
||||
foreach ($checks as $label => $check) {
|
||||
if (! self::passes($check)) {
|
||||
$failed[] = (string) $label;
|
||||
}
|
||||
}
|
||||
|
||||
return $failed;
|
||||
}
|
||||
|
||||
/** @return list<Problem> Code and mirror problems with their features – what the release matrix is built of. */
|
||||
public function problems(): array
|
||||
{
|
||||
return array_merge($this->codeProblems(), $this->mirrorProblems());
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls (missing or other argument count: call; no reference: reference-missing; other signature: signature)
|
||||
* and constants. Reflection only – cheap enough for every request.
|
||||
*
|
||||
* @return list<Problem>
|
||||
*/
|
||||
public function codeProblems(): array
|
||||
{
|
||||
$problems = [];
|
||||
foreach ($this->manifests as $manifest) {
|
||||
foreach ($manifest->calls as $call) {
|
||||
$problem = $this->callProblem($manifest, $call);
|
||||
if ($problem !== null) {
|
||||
$problems[] = $problem;
|
||||
}
|
||||
}
|
||||
foreach ($manifest->constants as $constant) {
|
||||
if (! defined($constant->name)) {
|
||||
$problems[] = new Problem(
|
||||
$manifest->area . ': ' . $constant->label(),
|
||||
$manifest->featuresOf($constant),
|
||||
Problem::CONSTANT
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $problems;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mirrors that differ from their reference, cannot be read or have no reference – kind mirror in every case:
|
||||
* a mirror repeats how Divi writes, so it never runs unverified, whatever the release. Reads Divi files; the
|
||||
* resolver caches the result.
|
||||
*
|
||||
* @return list<Problem>
|
||||
*/
|
||||
public function mirrorProblems(): array
|
||||
{
|
||||
$problems = [];
|
||||
foreach ($this->manifests as $manifest) {
|
||||
foreach ($manifest->mirrors as $mirror) {
|
||||
if (! self::passes(fn (): bool => $this->matches($mirror))) {
|
||||
$problems[] = new Problem(
|
||||
self::mirrorLabel($manifest, $mirror),
|
||||
$manifest->featuresOf($mirror),
|
||||
Problem::MIRROR
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $problems;
|
||||
}
|
||||
|
||||
/** The function or method exists and takes the number of arguments the adapter passes. */
|
||||
public static function accepts(Call $call): bool
|
||||
{
|
||||
$class = $call->className();
|
||||
if ($class === null) {
|
||||
if (! function_exists($call->member())) {
|
||||
return false;
|
||||
}
|
||||
$reflection = new ReflectionFunction($call->member());
|
||||
} else {
|
||||
if (! method_exists($class, $call->member())) {
|
||||
return false;
|
||||
}
|
||||
$reflection = new ReflectionMethod($class, $call->member());
|
||||
}
|
||||
|
||||
return $call->args === null
|
||||
|| ($reflection->getNumberOfRequiredParameters() <= $call->args
|
||||
&& ($reflection->isVariadic() || $reflection->getNumberOfParameters() >= $call->args));
|
||||
}
|
||||
|
||||
private function callProblem(Manifest $manifest, Call $call): ?Problem
|
||||
{
|
||||
$label = $manifest->area . ': ' . $call->label();
|
||||
$features = $manifest->featuresOf($call);
|
||||
if (! self::passes(static fn (): bool => self::accepts($call))) {
|
||||
return new Problem($label, $features, Problem::CALL);
|
||||
}
|
||||
|
||||
$expected = $this->expected()[DiviReference::signatureKey($call)] ?? null;
|
||||
if ($expected === null) {
|
||||
return new Problem($label . ' – Referenz der Signatur fehlt', $features, Problem::REFERENCE_MISSING);
|
||||
}
|
||||
|
||||
return self::passes(static fn (): bool => SignatureFingerprint::of($call) === $expected)
|
||||
? null
|
||||
: new Problem($label . ' – Signatur geändert', $features, Problem::SIGNATURE);
|
||||
}
|
||||
|
||||
/** The mirror's reference exists and the installed source has the same fingerprint. */
|
||||
private function matches(Mirror $mirror): bool
|
||||
{
|
||||
$expected = $this->expected()[$mirror->id] ?? null;
|
||||
|
||||
return $expected !== null && ($this->current)($mirror) === $expected;
|
||||
}
|
||||
|
||||
/** @return array<string, string> */
|
||||
private function expected(): array
|
||||
{
|
||||
return $this->expected ??= DiviReference::expected();
|
||||
}
|
||||
|
||||
private static function mirrorLabel(Manifest $manifest, Mirror $mirror): string
|
||||
{
|
||||
return sprintf('%s: Nachbau von %s – %s', $manifest->area, $mirror->label(), $mirror->what);
|
||||
}
|
||||
|
||||
/** @param callable(): bool $check A check that throws failed. */
|
||||
private static function passes(callable $check): bool
|
||||
{
|
||||
try {
|
||||
return (bool) $check();
|
||||
} catch (Throwable) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static function installed(Mirror $mirror): ?string
|
||||
{
|
||||
$dir = DiviEnvironment::themeDir();
|
||||
|
||||
return $dir === null
|
||||
? null
|
||||
: SourceFingerprint::of($dir . '/' . $mirror->file, $mirror->function, $mirror->class)->hash;
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Release;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* A Divi function whose storage logic an adapter repeats instead of calling it (spec §4.1 "mirrors"). Its
|
||||
* fingerprint (SourceFingerprint) must match the one in fingerprints.json, else Divi changed what we copy.
|
||||
*/
|
||||
final class Mirror implements Requirement
|
||||
{
|
||||
/**
|
||||
* @param string $id Key in fingerprints.json and name of tests/Fixtures/divi/reference/<id>.txt.
|
||||
* @param string $file Divi file, relative to the Divi theme directory.
|
||||
* @param string $function Function or method name.
|
||||
* @param string|null $class Class of a method – the short name is enough (the file fixes where it
|
||||
* lives, and a short name is no Divi name for DiviBoundaryTest); null for a
|
||||
* function.
|
||||
* @param string $what What the plugin repeats (German, for reports).
|
||||
* @param list<DiviFeature> $features [] = the default of the manifest.
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly string $id,
|
||||
public readonly string $file,
|
||||
public readonly string $function,
|
||||
public readonly ?string $class,
|
||||
public readonly string $what,
|
||||
private readonly array $features = []
|
||||
) {
|
||||
}
|
||||
|
||||
public function features(): array
|
||||
{
|
||||
return $this->features;
|
||||
}
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
$class = $this->class === null ? '' : SourceFingerprint::shortName($this->class) . '::';
|
||||
|
||||
return $class . $this->function . '()';
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Release;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* One failed manifest check and the Divi features it concerns (Manifest::featuresOf()). The kind decides how the
|
||||
* resolver weighs it (E1): call, mirror and constant lock their features for every Divi version, released or not;
|
||||
* signature and reference-missing only matter for a version without release (automatic release). A problem without
|
||||
* features concerns every feature (fail closed).
|
||||
*/
|
||||
final class Problem
|
||||
{
|
||||
public const CALL = 'call';
|
||||
public const SIGNATURE = 'signature';
|
||||
public const MIRROR = 'mirror';
|
||||
public const CONSTANT = 'constant';
|
||||
public const REFERENCE_MISSING = 'reference-missing';
|
||||
public const RESOLVER = 'resolver';
|
||||
|
||||
private const KINDS = [
|
||||
self::CALL,
|
||||
self::SIGNATURE,
|
||||
self::MIRROR,
|
||||
self::CONSTANT,
|
||||
self::REFERENCE_MISSING,
|
||||
self::RESOLVER,
|
||||
];
|
||||
private const HARD = [self::CALL, self::MIRROR, self::CONSTANT, self::RESOLVER];
|
||||
|
||||
/**
|
||||
* @param string $label German, e.g. "Seiten: et_x()" (the labels of ManifestCheck).
|
||||
* @param list<DiviFeature> $features
|
||||
* @param string $kind One of the constants above.
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly string $label,
|
||||
public readonly array $features,
|
||||
public readonly string $kind
|
||||
) {
|
||||
}
|
||||
|
||||
/** Locks its features whatever the release (the code no longer fits Divi or a mirror differs). */
|
||||
public function isHard(): bool
|
||||
{
|
||||
return in_array($this->kind, self::HARD, true);
|
||||
}
|
||||
|
||||
public function concerns(DiviFeature $feature): bool
|
||||
{
|
||||
return $this->features === [] || in_array($feature, $this->features, true);
|
||||
}
|
||||
|
||||
/** @return array{label: string, kind: string, features: list<string>} For the transient. */
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'label' => $this->label,
|
||||
'kind' => $this->kind,
|
||||
'features' => array_map(static fn (DiviFeature $feature): string => $feature->value, $this->features),
|
||||
];
|
||||
}
|
||||
|
||||
/** The problem of toArray(), or null for anything else (an unknown kind or feature included). */
|
||||
public static function fromArray(mixed $row): ?self
|
||||
{
|
||||
if (
|
||||
! is_array($row)
|
||||
|| ! is_string($row['label'] ?? null)
|
||||
|| ! in_array($row['kind'] ?? null, self::KINDS, true)
|
||||
|| ! is_array($row['features'] ?? null)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$features = [];
|
||||
foreach ($row['features'] as $value) {
|
||||
$feature = is_string($value) ? DiviFeature::tryFrom($value) : null;
|
||||
if ($feature === null) {
|
||||
return null;
|
||||
}
|
||||
$features[] = $feature;
|
||||
}
|
||||
|
||||
return new self($row['label'], $features, $row['kind']);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,9 @@ declare(strict_types=1);
|
||||
namespace LIT\MCP\Integrations\Divi\Release;
|
||||
|
||||
use Closure;
|
||||
use LIT\MCP\Compatibility\Capability;
|
||||
use LIT\MCP\Compatibility\CompatibilityMatrix;
|
||||
use LIT\MCP\Compatibility\FeatureStatus;
|
||||
use LIT\MCP\Log;
|
||||
use Throwable;
|
||||
|
||||
@@ -37,7 +40,10 @@ final class ReleaseGate
|
||||
self::$status = (self::$resolver ?? static fn (): ReleaseStatus => (new ReleaseResolver())->resolve())();
|
||||
} catch (Throwable $e) {
|
||||
Log::failure('divi release status', $e);
|
||||
self::$status = ReleaseStatus::of('', CompatibilityMatrix::allIncompatible(self::UNRESOLVED));
|
||||
self::$status = ReleaseStatus::of(
|
||||
'',
|
||||
CompatibilityMatrix::allIncompatible(DiviFeature::cases(), self::UNRESOLVED)
|
||||
);
|
||||
}
|
||||
|
||||
return self::$status;
|
||||
@@ -107,11 +113,17 @@ final class ReleaseGate
|
||||
{
|
||||
$status = self::status();
|
||||
$locked = $status->matrix->locked($features);
|
||||
if ($locked === []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $locked === [] ? null : new DiviLocked(
|
||||
$status,
|
||||
array_map(static fn (FeatureStatus $feature): DiviFeature => $feature->feature, $locked)
|
||||
);
|
||||
$named = array_values(array_filter(
|
||||
array_map(static fn (FeatureStatus $feature): Capability => $feature->feature, $locked),
|
||||
static fn (Capability $capability): bool => $capability instanceof DiviFeature
|
||||
));
|
||||
|
||||
// Fail closed: the lock stays; without a DiviFeature among its capabilities it names the features asked for.
|
||||
return new DiviLocked($status, $named === [] ? $features : $named);
|
||||
}
|
||||
|
||||
public static function whileProbing(int $postId, callable $write): mixed
|
||||
|
||||
@@ -5,6 +5,9 @@ declare(strict_types=1);
|
||||
namespace LIT\MCP\Integrations\Divi\Release;
|
||||
|
||||
use Closure;
|
||||
use LIT\MCP\Compatibility\CompatibilityMatrix;
|
||||
use LIT\MCP\Compatibility\ManifestCheck;
|
||||
use LIT\MCP\Compatibility\Problem;
|
||||
use LIT\MCP\Plugin;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
@@ -44,15 +47,19 @@ final class ReleaseResolver
|
||||
public function resolve(): ReleaseStatus
|
||||
{
|
||||
$version = ($this->version ?? DiviEnvironment::version(...))() ?? '';
|
||||
$check = $this->check ?? new ManifestCheck();
|
||||
$check = $this->check ?? DiviManifestCheck::create();
|
||||
$problems = array_merge($check->codeProblems(), $this->mirrorProblems($check, $version));
|
||||
|
||||
if (in_array($version, DiviReleases::TESTED, true)) {
|
||||
return ReleaseStatus::of($version, self::matrix($problems, ReleaseStatus::SOURCE_CODE, false));
|
||||
return ReleaseStatus::of($version, self::matrix($problems, CompatibilityMatrix::SOURCE_CODE, false));
|
||||
}
|
||||
$entry = $version === '' ? null : ($this->store ?? new ReleaseStore())->find($version, Plugin::VERSION);
|
||||
if ($entry !== null) {
|
||||
return ReleaseStatus::of($version, self::matrix($problems, ReleaseStatus::SOURCE_DATABASE, false), $entry);
|
||||
return ReleaseStatus::of(
|
||||
$version,
|
||||
self::matrix($problems, CompatibilityMatrix::SOURCE_DATABASE, false),
|
||||
$entry
|
||||
);
|
||||
}
|
||||
|
||||
return ReleaseStatus::of($version, self::matrix($problems, null, $version !== '' && self::automatic()));
|
||||
@@ -81,60 +88,10 @@ final class ReleaseResolver
|
||||
return apply_filters(self::AUTO_RELEASE_FILTER, true) === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<Problem> $problems
|
||||
* @param string|null $source SOURCE_CODE or SOURCE_DATABASE for a released version, else null.
|
||||
* @param bool $automatic The automatic release applies (no release, known version, filter on).
|
||||
*/
|
||||
/** @param list<Problem> $problems */
|
||||
private static function matrix(array $problems, ?string $source, bool $automatic): CompatibilityMatrix
|
||||
{
|
||||
$features = [];
|
||||
foreach (DiviFeature::cases() as $feature) {
|
||||
$concerning = array_values(array_filter(
|
||||
$problems,
|
||||
static fn (Problem $problem): bool => $problem->concerns($feature)
|
||||
));
|
||||
$features[$feature->value] = self::feature($feature, $concerning, $source, $automatic);
|
||||
}
|
||||
|
||||
return new CompatibilityMatrix($features);
|
||||
}
|
||||
|
||||
/** @param list<Problem> $problems The problems concerning $feature. */
|
||||
private static function feature(
|
||||
DiviFeature $feature,
|
||||
array $problems,
|
||||
?string $source,
|
||||
bool $automatic
|
||||
): FeatureStatus {
|
||||
$hard = array_filter($problems, static fn (Problem $problem): bool => $problem->isHard());
|
||||
if ($hard !== []) {
|
||||
return new FeatureStatus($feature, ReleaseState::Incompatible, self::labels($hard));
|
||||
}
|
||||
if ($source !== null) {
|
||||
return new FeatureStatus($feature, ReleaseState::Released, [], $source);
|
||||
}
|
||||
if (! $automatic) {
|
||||
return new FeatureStatus($feature, ReleaseState::Untested, self::labels($problems));
|
||||
}
|
||||
|
||||
$changed = array_filter($problems, static fn (Problem $problem): bool => $problem->kind === Problem::SIGNATURE);
|
||||
if ($changed !== []) {
|
||||
return new FeatureStatus($feature, ReleaseState::Incompatible, self::labels($changed));
|
||||
}
|
||||
|
||||
return $problems === []
|
||||
? new FeatureStatus($feature, ReleaseState::Released, [], ReleaseStatus::SOURCE_AUTO)
|
||||
: new FeatureStatus($feature, ReleaseState::Untested, self::labels($problems));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, Problem> $problems
|
||||
* @return list<string>
|
||||
*/
|
||||
private static function labels(array $problems): array
|
||||
{
|
||||
return array_values(array_map(static fn (Problem $problem): string => $problem->label, $problems));
|
||||
return CompatibilityMatrix::fromProblems(DiviFeature::cases(), $problems, $source, $automatic);
|
||||
}
|
||||
|
||||
/** @return list<Problem> */
|
||||
@@ -174,7 +131,7 @@ final class ReleaseResolver
|
||||
|
||||
$problems = [];
|
||||
foreach ($entry['problems'] as $row) {
|
||||
$problem = Problem::fromArray($row);
|
||||
$problem = Problem::fromArray($row, DiviFeature::tryFrom(...));
|
||||
if ($problem === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Release;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/** Spec §5.2. */
|
||||
enum ReleaseState: string
|
||||
{
|
||||
case Released = 'released';
|
||||
case Untested = 'untested';
|
||||
case Incompatible = 'incompatible';
|
||||
}
|
||||
@@ -5,6 +5,9 @@ declare(strict_types=1);
|
||||
namespace LIT\MCP\Integrations\Divi\Release;
|
||||
|
||||
use LIT\MCP\Abilities\Errors;
|
||||
use LIT\MCP\Compatibility\CompatibilityMatrix;
|
||||
use LIT\MCP\Compatibility\FeatureStatus;
|
||||
use LIT\MCP\Compatibility\ReleaseState;
|
||||
use LogicException;
|
||||
use WP_Error;
|
||||
|
||||
@@ -16,9 +19,9 @@ defined('ABSPATH') || exit;
|
||||
*/
|
||||
final class ReleaseStatus
|
||||
{
|
||||
public const SOURCE_CODE = 'code';
|
||||
public const SOURCE_DATABASE = 'database';
|
||||
public const SOURCE_AUTO = 'auto';
|
||||
public const SOURCE_CODE = CompatibilityMatrix::SOURCE_CODE;
|
||||
public const SOURCE_DATABASE = CompatibilityMatrix::SOURCE_DATABASE;
|
||||
public const SOURCE_AUTO = CompatibilityMatrix::SOURCE_AUTO;
|
||||
|
||||
private const FEATURE_UNTESTED = 'Achtung: %1$s ist auf dieser Website für %2$s noch nicht für LIT MCP '
|
||||
. 'freigegeben. Das ist gesperrt, bis ein Administrator unter Einstellungen → LIT MCP → Divi die Prüfung '
|
||||
@@ -52,7 +55,7 @@ final class ReleaseStatus
|
||||
if ($matrix !== null && $matrix->state() !== $state) {
|
||||
throw new LogicException('the matrix is ' . $matrix->state()->value . ', not ' . $state->value);
|
||||
}
|
||||
$this->matrix = $matrix ?? CompatibilityMatrix::uniform($state, $problems, $source);
|
||||
$this->matrix = $matrix ?? CompatibilityMatrix::uniform(DiviFeature::cases(), $state, $problems, $source);
|
||||
}
|
||||
|
||||
/** @param array<string, mixed>|null $release The database entry of a SOURCE_DATABASE release. */
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Release;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/** One thing an adapter needs from Divi (a call, a mirrored function or a constant) and the features it serves. */
|
||||
interface Requirement
|
||||
{
|
||||
/** Name for reports, e.g. "GlobalPreset::get_data()" (without the manifest area). */
|
||||
public function label(): string;
|
||||
|
||||
/**
|
||||
* The features this requirement serves itself; [] = the default of the manifest that lists it
|
||||
* (Manifest::featuresOf() resolves that).
|
||||
*
|
||||
* @return list<DiviFeature>
|
||||
*/
|
||||
public function features(): array;
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
<?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).
|
||||
*
|
||||
* Type strings come from Reflection and differ between PHP versions (e.g. `iterable` reads as `Traversable|array`
|
||||
* from 8.2). The reference was computed under PHP 8.4; another PHP version can therefore report a spurious signature
|
||||
* problem – it only blocks the automatic release (fail closed), never releases; --update-reference regenerates it.
|
||||
*/
|
||||
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,185 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Release;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* Fingerprint of one Divi function or method (spec §4.2), read with token_get_all(): the file is never included,
|
||||
* so functions Divi loads only in some requests (theme-builder/api.php, admin-ajax only) are readable in every
|
||||
* request. The hash covers the tokens from "function" to the closing brace without comments and whitespace –
|
||||
* new comments or reformatting keep it, any code change changes it. $source is the verbatim text of those
|
||||
* tokens for the human diff (wp lit-mcp divi-check --diff).
|
||||
*/
|
||||
final class SourceFingerprint
|
||||
{
|
||||
private const IGNORED = [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT];
|
||||
|
||||
private const TYPES = [T_CLASS, T_TRAIT, T_INTERFACE, T_ENUM];
|
||||
|
||||
private function __construct(
|
||||
public readonly string $hash,
|
||||
public readonly string $source,
|
||||
public readonly int $startLine,
|
||||
public readonly int $endLine
|
||||
) {
|
||||
}
|
||||
|
||||
/** @throws RuntimeException When the file is unreadable or holds no such function. */
|
||||
public static function of(string $file, string $function, ?string $class = null): self
|
||||
{
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Reading local PHP file, not remote content.
|
||||
$source = is_file($file) && is_readable($file) ? file_get_contents($file) : false;
|
||||
if (! is_string($source)) {
|
||||
throw new RuntimeException(sprintf('%s is not readable', $file));
|
||||
}
|
||||
|
||||
return self::fromSource($source, $function, $class) ?? throw new RuntimeException(sprintf(
|
||||
'%s not found in %s',
|
||||
$class === null ? $function . '()' : self::shortName($class) . '::' . $function . '()',
|
||||
$file
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* The function $function outside every class body, or – with $class – the method $function declared directly
|
||||
* in the class (trait, interface, enum) named like $class's last segment. The namespace is not compared: the
|
||||
* file path of the manifest fixes where to look.
|
||||
*/
|
||||
public static function fromSource(string $source, string $function, ?string $class = null): ?self
|
||||
{
|
||||
$tokens = token_get_all($source);
|
||||
$wanted = $class === null ? null : self::shortName($class);
|
||||
$depth = 0;
|
||||
/** @var list<array{0: int, 1: string}> $bodies Open class bodies: [brace depth, name]. */
|
||||
$bodies = [];
|
||||
$pending = null;
|
||||
|
||||
foreach ($tokens as $i => $token) {
|
||||
$id = is_array($token) ? $token[0] : null;
|
||||
$text = is_array($token) ? $token[1] : $token;
|
||||
|
||||
if ($id !== null && in_array($id, self::TYPES, true) && self::previousId($tokens, $i) !== T_DOUBLE_COLON) {
|
||||
$pending = self::nextName($tokens, $i) ?? '';
|
||||
continue;
|
||||
}
|
||||
if ($text === '{' || $id === T_CURLY_OPEN || $id === T_DOLLAR_OPEN_CURLY_BRACES) {
|
||||
++$depth;
|
||||
if ($pending !== null && $text === '{') {
|
||||
$bodies[] = [$depth, $pending];
|
||||
$pending = null;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if ($text === '}') {
|
||||
if ($bodies !== [] && $bodies[count($bodies) - 1][0] === $depth) {
|
||||
array_pop($bodies);
|
||||
}
|
||||
--$depth;
|
||||
continue;
|
||||
}
|
||||
if ($id !== T_FUNCTION || self::previousId($tokens, $i) === T_USE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$name = self::nextName($tokens, $i);
|
||||
if ($name === null || strcasecmp($name, $function) !== 0) {
|
||||
continue;
|
||||
}
|
||||
$body = $bodies === [] ? null : $bodies[count($bodies) - 1];
|
||||
$matches = $wanted === null
|
||||
? $body === null
|
||||
: $body !== null && $body[0] === $depth && strcasecmp($body[1], $wanted) === 0;
|
||||
if ($matches) {
|
||||
return self::capture($tokens, $i);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @param array<int, mixed> $tokens */
|
||||
private static function capture(array $tokens, int $start): ?self
|
||||
{
|
||||
$startLine = (int) $tokens[$start][2];
|
||||
$parts = [];
|
||||
$verbatim = '';
|
||||
$depth = 0;
|
||||
$opened = false;
|
||||
$count = count($tokens);
|
||||
|
||||
for ($i = $start; $i < $count; ++$i) {
|
||||
$token = $tokens[$i];
|
||||
$id = is_array($token) ? $token[0] : null;
|
||||
$text = is_array($token) ? $token[1] : $token;
|
||||
$verbatim .= $text;
|
||||
if ($id === null || ! in_array($id, self::IGNORED, true)) {
|
||||
$parts[] = $text;
|
||||
}
|
||||
|
||||
if ($text === '{' || $id === T_CURLY_OPEN || $id === T_DOLLAR_OPEN_CURLY_BRACES) {
|
||||
++$depth;
|
||||
$opened = true;
|
||||
} elseif ($text === '}') {
|
||||
--$depth;
|
||||
if ($opened && $depth === 0) {
|
||||
return new self(
|
||||
hash('sha256', implode(' ', $parts)),
|
||||
$verbatim,
|
||||
$startLine,
|
||||
$startLine + substr_count($verbatim, "\n")
|
||||
);
|
||||
}
|
||||
} elseif ($text === ';' && ! $opened) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The next name after $i (skipping whitespace, comments and "&"), null when something else follows. */
|
||||
private static function nextName(array $tokens, int $i): ?string
|
||||
{
|
||||
$count = count($tokens);
|
||||
for ($j = $i + 1; $j < $count; ++$j) {
|
||||
$token = $tokens[$j];
|
||||
if (is_array($token) && in_array($token[0], self::IGNORED, true)) {
|
||||
continue;
|
||||
}
|
||||
if ($token === '&' || (is_array($token) && $token[1] === '&')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return is_array($token) && $token[0] === T_STRING ? $token[1] : null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Token ID of the previous significant token, null for a single-character token or none. */
|
||||
private static function previousId(array $tokens, int $i): ?int
|
||||
{
|
||||
for ($j = $i - 1; $j >= 0; --$j) {
|
||||
$token = $tokens[$j];
|
||||
if (is_array($token) && in_array($token[0], self::IGNORED, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return is_array($token) ? $token[0] : null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static function shortName(string $class): string
|
||||
{
|
||||
$position = strrpos($class, '\\');
|
||||
|
||||
return $position === false ? $class : substr($class, $position + 1);
|
||||
}
|
||||
}
|
||||
@@ -7,10 +7,11 @@ namespace LIT\MCP\Integrations\Divi\Schema;
|
||||
use ET\Builder\Packages\ModuleLibrary\ModuleRegistration;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\GlobalInstance;
|
||||
use LIT\MCP\Integrations\Divi\Library\DiviLibraryStore;
|
||||
use LIT\MCP\Integrations\Divi\Release\Call;
|
||||
use LIT\MCP\Compatibility\Call;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviAdapter;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviAreas;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use LIT\MCP\Integrations\Divi\Release\Manifest;
|
||||
use LIT\MCP\Compatibility\Manifest;
|
||||
use LIT\MCP\Integrations\Divi\ThemeBuilder\DiviThemeBuilder;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
@@ -39,7 +40,7 @@ class DiviModules implements DiviAdapter
|
||||
public static function manifest(): Manifest
|
||||
{
|
||||
return new Manifest(
|
||||
Manifest::CATALOG,
|
||||
DiviAreas::CATALOG,
|
||||
calls: [
|
||||
new Call(ModuleRegistration::class . '::get_all_core_modules_metadata', 0),
|
||||
new Call(ModuleRegistration::class . '::get_default_attrs', 2),
|
||||
|
||||
@@ -4,12 +4,13 @@ declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\ThemeBuilder;
|
||||
|
||||
use LIT\MCP\Integrations\Divi\Release\Call;
|
||||
use LIT\MCP\Compatibility\Call;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviAdapter;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviAreas;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviLocked;
|
||||
use LIT\MCP\Integrations\Divi\Release\Manifest;
|
||||
use LIT\MCP\Integrations\Divi\Release\Mirror;
|
||||
use LIT\MCP\Compatibility\Manifest;
|
||||
use LIT\MCP\Compatibility\Mirror;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
@@ -83,7 +84,7 @@ class DiviThemeBuilder implements DiviAdapter
|
||||
$dir = 'includes/builder/frontend-builder/theme-builder/';
|
||||
|
||||
return new Manifest(
|
||||
Manifest::THEME_BUILDER,
|
||||
DiviAreas::THEME_BUILDER,
|
||||
calls: [
|
||||
// liveId(): reads the live post, or creates it like Divi's first save.
|
||||
new Call(
|
||||
|
||||
Reference in New Issue
Block a user