Files
WP-LIT-MCP/src/Integrations/Divi/DiviIntegration.php
T
Jan-Niclas LoosenandClaude Opus 5.5 b40b81f585 refactor(divi): DiviTab geht im Reiter „Compatibility“ auf
Der eigene Divi-Reiter entfällt. DiviAdminExtras behält Admin-Hinweis, Orphan-Bereinigung vor der Ausgabe (lit_mcp_compat_profile_load), Hinweis zum nächsten Schritt und zur privaten Testseite (lit_mcp_compat_profile_intro), Zustandstexte mit Quelle (lit_mcp_compat_explanation), Automatik-Notiz und Visual-Builder-Hinweis (lit_mcp_compat_profile_extra) sowie die Spalten Quelle, Probleme und Betroffene Tools über die neue, gegen fehlerhafte Callbacks abgesicherte Erweiterung lit_mcp_compat_capability_columns. Texte, Doku, CSS (lit-mcp-compat-*) und Smoke auf den neuen Reiter umgestellt, tote divi_*-Hinweise entfernt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 13:32:29 +01:00

510 lines
21 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi;
use Closure;
use LIT\MCP\Abilities\Ability;
use LIT\MCP\Abilities\ToolAvailability;
use LIT\MCP\Admin\CheckRoute;
use LIT\MCP\ChangeLog\ChangeLog;
use LIT\MCP\Compatibility\CompatibilityCheck;
use LIT\MCP\Compatibility\Profile;
use LIT\MCP\Compatibility\ProvidesCompatibility;
use LIT\MCP\Integrations\Bootable;
use LIT\MCP\Integrations\Divi\Admin\DiviAdminExtras;
use LIT\MCP\Integrations\Divi\Cli\DiviCheckCommand;
use LIT\MCP\Integrations\Divi\BlockTree\Parser;
use LIT\MCP\Integrations\Divi\Design\ColorRestorePlanner;
use LIT\MCP\Integrations\Divi\Design\DesignSystemReader;
use LIT\MCP\Integrations\Divi\Design\DesignSystemWriter;
use LIT\MCP\Integrations\Divi\Design\DiviColorsSetAbility;
use LIT\MCP\Integrations\Divi\Design\DiviDesignSystemAbility;
use LIT\MCP\Integrations\Divi\Design\DesignChange;
use LIT\MCP\Integrations\Divi\Design\DesignRights;
use LIT\MCP\Integrations\Divi\Design\DiviPresetGetAbility;
use LIT\MCP\Integrations\Divi\Design\DiviDesignRestoreAbility;
use LIT\MCP\Integrations\Divi\Design\DiviDesignSnapshotsAbility;
use LIT\MCP\Integrations\Divi\Design\DiviDesignDetachAbility;
use LIT\MCP\Integrations\Divi\Design\GlobalColorsAdapter;
use LIT\MCP\Integrations\Divi\Design\GlobalVariablesAdapter;
use LIT\MCP\Integrations\Divi\Design\DiviPresetsAssignAbility;
use LIT\MCP\Integrations\Divi\Design\DiviPresetsDeleteAbility;
use LIT\MCP\Integrations\Divi\Design\DiviPresetsSetAbility;
use LIT\MCP\Integrations\Divi\Design\DiviVariablesSetAbility;
use LIT\MCP\Integrations\Divi\Design\DiviFontsUploadAbility;
use LIT\MCP\Integrations\Divi\Design\DiviThemeOptionsGetAbility;
use LIT\MCP\Integrations\Divi\Design\DiviThemeOptionsUpdateAbility;
use LIT\MCP\Integrations\Divi\Design\FontCatalog;
use LIT\MCP\Integrations\Divi\Design\FontInstaller;
use LIT\MCP\Integrations\Divi\Design\PageRewriter;
use LIT\MCP\Integrations\Divi\Design\PresetAssignment;
use LIT\MCP\Integrations\Divi\Design\PresetComposer;
use LIT\MCP\Integrations\Divi\Design\PresetRestorePlanner;
use LIT\MCP\Integrations\Divi\Design\SnapshotStore;
use LIT\MCP\Integrations\Divi\Design\ThemeOptionsAdapter;
use LIT\MCP\Integrations\Divi\Design\CustomCssGuard;
use LIT\MCP\Integrations\Divi\Design\CustomCssRestorePlanner;
use LIT\MCP\Integrations\Divi\Design\CustomCssStore;
use LIT\MCP\Integrations\Divi\Design\DiviCustomCssGetAbility;
use LIT\MCP\Integrations\Divi\Design\DiviCustomCssUpdateAbility;
use LIT\MCP\Integrations\Divi\Design\ThemeOptionsRestorePlanner;
use LIT\MCP\Integrations\Divi\Design\VariableRestorePlanner;
use LIT\MCP\Integrations\Divi\Library\DiviLibraryStore;
use LIT\MCP\Integrations\Divi\Library\DiviRoles;
use LIT\MCP\Integrations\Divi\Library\GlobalEdits;
use LIT\MCP\Integrations\Divi\Library\GlobalUsages;
use LIT\MCP\Integrations\Divi\Library\LibraryAccess;
use LIT\MCP\Integrations\Divi\Library\LibraryIntegration;
use LIT\MCP\Integrations\Divi\Library\SimilarSections;
use LIT\MCP\Integrations\Divi\Pages\DiviComposition;
use LIT\MCP\Integrations\Divi\Pages\DiviContentComposer;
use LIT\MCP\Integrations\Divi\Pages\DiviDeleteAbility;
use LIT\MCP\Integrations\Divi\Pages\DiviDuplicateNodeAbility;
use LIT\MCP\Integrations\Divi\Pages\DiviDynamicSourcesAbility;
use LIT\MCP\Integrations\Divi\Pages\DynamicFields;
use LIT\MCP\Integrations\Divi\Pages\DiviInsertAbility;
use LIT\MCP\Integrations\Divi\Pages\DiviMoveAbility;
use LIT\MCP\Integrations\Divi\Pages\DiviNodeCopyAbility;
use LIT\MCP\Integrations\Divi\Pages\DiviNodeGetAbility;
use LIT\MCP\Integrations\Divi\Pages\DiviOutlineAbility;
use LIT\MCP\Integrations\Divi\Pages\DiviPageLoader;
use LIT\MCP\Integrations\Divi\Pages\DiviReplaceTextAbility;
use LIT\MCP\Integrations\Divi\Pages\DiviSaver;
use LIT\MCP\Integrations\Divi\Pages\DiviUpdateAbility;
use LIT\MCP\Integrations\Divi\Pages\DiviWriter;
use LIT\MCP\Integrations\Divi\Pages\DynamicContentMap;
use LIT\MCP\Integrations\Divi\Release\FeatureOverview;
use LIT\MCP\Integrations\Divi\Release\DiviProfile;
use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
use LIT\MCP\Integrations\Divi\Schema\DiviCatalogAbility;
use LIT\MCP\Integrations\Divi\Schema\DiviModules;
use LIT\MCP\Integrations\Divi\ThemeBuilder\DiviThemeBuilder;
use LIT\MCP\Integrations\Divi\ThemeBuilder\ThemeBuilderAccess;
use LIT\MCP\Integrations\Divi\ThemeBuilder\ThemeBuilderEdits;
use LIT\MCP\Integrations\Divi\ThemeBuilder\ThemeBuilderIntegration;
use LIT\MCP\Integrations\Core\EnvironmentComponent;
use LIT\MCP\Integrations\Integration;
use LIT\MCP\Integrations\Media\MediaUsageSource;
use LIT\MCP\Integrations\Media\RemoteFileFetcher;
use LIT\MCP\Limits;
use LIT\MCP\Integrations\WordPress\BuilderDetector;
use LIT\MCP\Integrations\WordPress\Content\ContentComposer;
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
defined('ABSPATH') || exit;
/**
* Divi 5 pages (spec §5, Etappe 2). Only available when the Divi 5 builder is loaded
* (spec §2 principle 4); Divi 4 is never edited (spec §3.4). The write abilities share one
* DiviWriter; each binds the composition to its entry via DiviWriter::composition().
* contentComposer() gives lit/content-create the Divi short form (spec §6.2). boot() registers Divi's own hooks.
*/
final class DiviIntegration implements Integration, Bootable, ProvidesCompatibility
{
private DiviModules $modules;
private DesignSystemReader $design;
private LibraryAccess $access;
private ThemeBuilderAccess $themeBuilderAccess;
private DesignSystemWriter $designWriter;
private GlobalColorsAdapter $colorsAdapter;
private GlobalVariablesAdapter $variablesAdapter;
private ThemeOptionsAdapter $themeOptionsAdapter;
/** One per request: the catalog holds Divi's big font list (ruling W5). */
private FontCatalog $fontCatalog;
private FontInstaller $fontInstaller;
/** @var (Closure(): bool)|null Whether lit/divi-insert is enabled (ruling P9, E9, audit M2); null = always. */
private ?Closure $diviWritable;
/** @var (Closure(): bool)|null Whether lit/library-insert is enabled (audit L5); null = always. */
private ?Closure $libraryInsertable;
/**
* @param ToolAvailability|null $tools The Tools-tab state, asked at call time: the groups "Divi-Bibliothek" and
* "Divi Theme Builder" gate their entries in the Divi tools (ruling P9, E9), lit/divi-insert gates
* lit/divi-node-copy, lit-library-insert and content-create's divi input (audit M2), lit/library-insert
* gates lit/divi-node-copy from a library entry (audit L5). null = no Tools tab: everything allowed.
* @param (Closure(): list<Ability>)|null $allAbilities The abilities of every integration (Plugin passes
* IntegrationRegistry::abilities()); the tools a Divi feature affects are found among them (E3).
*/
public function __construct(
private ContentSchema $schema,
private ChangeLog $log,
private ?ToolAvailability $tools = null,
private ?Closure $allAbilities = null
) {
$this->modules = new DiviModules();
$this->design = new DesignSystemReader();
$this->designWriter = new DesignSystemWriter();
$this->colorsAdapter = new GlobalColorsAdapter();
$this->variablesAdapter = new GlobalVariablesAdapter();
$this->themeOptionsAdapter = new ThemeOptionsAdapter();
$this->fontCatalog = new FontCatalog();
$this->fontInstaller = new FontInstaller();
$this->diviWritable = $this->gate(static fn (ToolAvailability $t): bool => $t->allows('lit/divi-insert'));
$this->libraryInsertable = $this->gate(
static fn (ToolAvailability $t): bool => $t->allows('lit/library-insert')
);
$roles = new DiviRoles();
$this->access = new LibraryAccess(
new DiviLibraryStore(),
$roles,
$this->gate(static fn (ToolAvailability $t): bool => $t->groupWritable(LibraryAccess::GROUP)),
$this->gate(static fn (ToolAvailability $t): bool => $t->groupEnabled(LibraryAccess::GROUP))
);
$this->themeBuilderAccess = new ThemeBuilderAccess(
new DiviThemeBuilder(),
$roles,
$this->gate(static fn (ToolAvailability $t): bool => $t->groupWritable(ThemeBuilderAccess::GROUP)),
$this->gate(static fn (ToolAvailability $t): bool => $t->groupEnabled(ThemeBuilderAccess::GROUP))
);
}
public function id(): string
{
return 'divi';
}
/** Ruling P13: whether this connection may see library IDs (library group visible, library right). */
public function libraryIdsVisible(): bool
{
return $this->access->revealsIds();
}
public function label(): string
{
return __('Divi', 'wp-lit-mcp');
}
public function isAvailable(): bool
{
return DiviModules::isAvailable();
}
public function compatibilities(): array
{
return [$this->profile()];
}
/**
* Divi's own post types (library, Theme Builder, canvases, speculation), which the content tools leave alone:
* ContentSchema's excluded post types. Static because the schema is built before this integration (Plugin).
*
* @return list<string>
*/
public static function internalPostTypes(): array
{
return DiviModules::INTERNAL_POST_TYPES;
}
/** Whether Divi's builder is switched on for $postType: ContentSchema::diviEnabled() (static, like above). */
public static function builderEnabledFor(string $postType): bool
{
return DiviModules::builderEnabledFor($postType);
}
/** Port for the content tools: Divi's builder flag and availability (PostPresenter). */
public function builderDetector(): BuilderDetector
{
return new DiviBuilderDetector();
}
/**
* Port for the media tools: places outside the library where Divi stores an attachment (its logo).
*
* @return list<MediaUsageSource>
*/
public function mediaUsageSources(): array
{
return [new DiviLogoUsage()];
}
/** Which tools each Divi feature affects – for lit-site-status and the tab "Compatibility" (E3). */
public function featureOverview(): FeatureOverview
{
return new FeatureOverview($this->allAbilities ?? static fn (): array => []);
}
/** Port for lit-site-status: Divi's version, missing internals, release status and features. */
public function environmentComponent(): EnvironmentComponent
{
return new DiviEnvironmentComponent(overview: $this->featureOverview());
}
/**
* Divi short form for lit/content-create; available() reports whether Divi 5 is loaded, writable() follows
* lit/divi-insert's Tools-tab state (security M2).
*/
public function contentComposer(): ContentComposer
{
return $this->shortForm($this->diviWritable);
}
/**
* Divi's own boot steps, registered whether or not Divi is loaded yet (Bootable): a theme update checks the
* fingerprints again at once (spec §5.4), the WP-CLI command (spec §7) and the Divi additions to the admin tab
* "Compatibility" (spec §6; the check route itself is generic, Admin\CheckRoute).
*/
public function boot(): void
{
// Spec §5.4: a theme update checks the Divi fingerprints again at once (the state key would, too).
add_action('upgrader_process_complete', static function (): void {
ReleaseGate::invalidate(mirrors: true);
});
// The admin check runs as a generic REST route (Admin\CheckRoute, registered by Plugin); a stored check or a
// release changes the state, so the gate forgets its status of this request.
add_action(CheckRoute::CHANGED_ACTION, static function (string $profileId): void {
if ($profileId === DiviProfile::ID) {
ReleaseGate::invalidate();
}
});
// Spec §7: the same check as WP-CLI command. Its short form has no Tools-tab gate: the check tests Divi,
// not this site's tool switches.
$profile = $this->profile();
DiviCheckCommand::register(static fn (): CompatibilityCheck => CompatibilityCheck::forProfile($profile));
// No tab of its own: notice, orphan cleanup, columns and notes join the tab "Compatibility".
(new DiviAdminExtras($this->featureOverview()))->register();
}
public function abilities(): array
{
$loader = $this->loader();
$writer = $this->writer($loader);
$customCss = new CustomCssStore();
return [
new DiviOutlineAbility($loader, $this->design),
new DiviNodeGetAbility($loader, $this->design),
new DiviCatalogAbility($this->modules),
new DiviDesignSystemAbility($this->design),
new DiviInsertAbility($writer),
new DiviUpdateAbility($writer),
new DiviMoveAbility($writer),
new DiviDeleteAbility($writer),
new DiviDuplicateNodeAbility($writer),
new DiviReplaceTextAbility($this->schema, $loader, $writer),
new DiviNodeCopyAbility($writer, $loader, $this->diviWritable, $this->libraryInsertable),
new DiviDynamicSourcesAbility(
$this->schema,
new DynamicContentMap(),
$this->access->roles(),
loader: $loader
),
new DiviPresetGetAbility($this->designWriter, $this->globalUsages()),
new DiviPresetsSetAbility(
$this->designWriter,
$this->designChange(),
$this->globalUsages(),
$loader,
$this->presetComposer(...),
DesignRights::unfilteredHtml()
),
new DiviPresetsDeleteAbility(
$this->designWriter,
$this->designChange(),
$this->globalUsages(),
DesignRights::unfilteredHtml()
),
new DiviDesignSnapshotsAbility(new SnapshotStore()),
new DiviDesignRestoreAbility(
$this->designChange(),
new SnapshotStore(),
new PresetRestorePlanner($this->designWriter, $this->globalUsages(), DesignRights::unfilteredHtml()),
new ColorRestorePlanner($this->designWriter, $this->colorsAdapter, $this->globalUsages()),
new VariableRestorePlanner($this->designWriter, $this->variablesAdapter, $this->globalUsages()),
new ThemeOptionsRestorePlanner($this->themeOptionsAdapter),
new CustomCssRestorePlanner($customCss, $this->themeOptionsAdapter)
),
new DiviPresetsAssignAbility(
$this->designWriter,
$this->pageRewriter($loader),
$this->presetAssignment(...)
),
new DiviDesignDetachAbility(
$this->designWriter,
$this->variablesAdapter,
$this->designChange(),
$this->globalUsages(),
$this->pageRewriter($loader),
DesignRights::unfilteredHtml()
),
new DiviColorsSetAbility(
$this->designWriter,
$this->colorsAdapter,
$this->designChange(),
$this->globalUsages()
),
new DiviVariablesSetAbility(
$this->designWriter,
$this->variablesAdapter,
$this->designChange(),
$this->globalUsages(),
$this->fontCatalog
),
new DiviThemeOptionsGetAbility($this->themeOptionsAdapter),
new DiviThemeOptionsUpdateAbility(
$this->themeOptionsAdapter,
$this->fontCatalog,
$this->designChange()
),
new DiviCustomCssGetAbility($customCss, $this->themeOptionsAdapter),
new DiviCustomCssUpdateAbility(
$customCss,
$this->themeOptionsAdapter,
$this->designChange(),
new CustomCssGuard()
),
new DiviFontsUploadAbility(
$this->fontCatalog,
$this->fontInstaller,
new RemoteFileFetcher(),
$this->log,
new Limits()
),
];
}
/**
* The group "Divi-Bibliothek" (lit/library-*), sharing this integration's library access and its
* $diviWritable gate (lit-library-insert follows lit/divi-insert, audit M2).
*/
public function library(): LibraryIntegration
{
$loader = $this->loader();
return new LibraryIntegration(
$this->access,
$loader,
$this->design,
$this->writer($loader),
new DiviSaver(),
$this->log,
$this->modules,
$this->diviWritable,
$this->schema,
$this->globalUsages()
);
}
/**
* The usages of library entries with this integration's Theme Builder access (ruling Q17 M1, review I-2):
* Theme Builder posts are named only while ThemeBuilderAccess::visible(), and a global edit that changes them
* needs the rights of every Theme Builder write (canUse(), in visible()) and a writable "Divi Theme Builder"
* group – as the Divi tools on those layouts.
*/
public function globalUsages(?\wpdb $db = null): GlobalUsages
{
return new GlobalUsages($db, $this->access->roles(), $this->themeBuilderAccess->usageState(...));
}
/** The group "Divi Theme Builder" (lit/tb-*), sharing this integration's Theme Builder access (Etappe 3b). */
public function themeBuilder(): ThemeBuilderIntegration
{
return new ThemeBuilderIntegration(
$this->themeBuilderAccess,
$this->composition(),
new DiviSaver(),
$this->log,
new DynamicFields($this->schema)
);
}
/**
* A gate on the Tools-tab state for the classes that take a (Closure(): bool)|null: asked at call time, null
* without $tools (always allowed).
*
* @param Closure(ToolAvailability): bool $ask
* @return (Closure(): bool)|null
*/
private function gate(Closure $ask): ?Closure
{
$tools = $this->tools;
return $tools === null ? null : static fn (): bool => $ask($tools);
}
/** @param (Closure(): bool)|null $writable */
/** The Divi profile; the check's write test composes through the short form without a Tools-tab gate. */
private function profile(): Profile
{
return DiviProfile::profile(fn (): DiviContentComposer => $this->shortForm(null));
}
private function shortForm(?Closure $writable): DiviContentComposer
{
return new DiviContentComposer($this->schema, $this->composition(), new DiviSaver(), $this->design, $writable);
}
/**
* The one wiring of the composition for every tool that composes (spec D7): presets and Divi's option groups come
* from the design system writer, so a stack or group preset in any write tool resolves against the live book.
*/
private function composition(): DiviComposition
{
return new DiviComposition($this->modules, $this->design, presetSource: $this->designWriter);
}
private function writer(DiviPageLoader $loader): DiviWriter
{
return new DiviWriter(
$loader,
new DiviSaver(),
$this->log,
$this->design,
$this->composition(),
new GlobalEdits($this->access, $this->globalUsages()),
new ThemeBuilderEdits($this->themeBuilderAccess, fields: new DynamicFields($this->schema)),
new SimilarSections($this->access)
);
}
private function designChange(): DesignChange
{
return new DesignChange(new SnapshotStore(), $this->log);
}
/**
* The page rewriter of lit/divi-presets-assign and lit/divi-design-detach (Etappe 4b, spec §6.6): usages with
* this integration's Theme Builder access, posts through $loader (shared with the other Divi tools).
*/
private function pageRewriter(DiviPageLoader $loader): PageRewriter
{
return new PageRewriter($this->globalUsages(), $loader, new DiviSaver(), $this->log);
}
private function presetComposer(): PresetComposer
{
$composition = $this->composition();
return new PresetComposer(
$composition->composer(),
$composition->storedGuard(),
$this->designWriter->presetAttrs(...),
$this->designWriter->groupsOf(...),
$this->modules->builderVersion(),
static fn (): int => (int) floor(microtime(true) * 1000)
);
}
/** A fresh PresetAssignment (own preset book) per lit/divi-presets-assign call, wired like the Composer's (D7). */
private function presetAssignment(): PresetAssignment
{
return $this->composition()->presets();
}
private function loader(): DiviPageLoader
{
return new DiviPageLoader($this->schema, new Parser(), $this->access, $this->themeBuilderAccess);
}
}