Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
414 lines
17 KiB
PHP
414 lines
17 KiB
PHP
<?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\SettingsPage;
|
||
use LIT\MCP\ChangeLog\ChangeLog;
|
||
use LIT\MCP\Integrations\Bootable;
|
||
use LIT\MCP\Integrations\Divi\Admin\DiviTab;
|
||
use LIT\MCP\Integrations\Divi\Cli\DiviCheckCommand;
|
||
use LIT\MCP\Integrations\Divi\BlockTree\Parser;
|
||
use LIT\MCP\Integrations\Divi\Design\DesignSystemReader;
|
||
use LIT\MCP\Integrations\Divi\Design\DesignSystemWriter;
|
||
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\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\PageRewriter;
|
||
use LIT\MCP\Integrations\Divi\Design\PresetAssignment;
|
||
use LIT\MCP\Integrations\Divi\Design\PresetComposer;
|
||
use LIT\MCP\Integrations\Divi\Design\SnapshotStore;
|
||
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\CompatibilityCheck;
|
||
use LIT\MCP\Integrations\Divi\Release\DiviCheckRoute;
|
||
use LIT\MCP\Integrations\Divi\Release\FeatureOverview;
|
||
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\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
|
||
{
|
||
private DiviModules $modules;
|
||
private DesignSystemReader $design;
|
||
private LibraryAccess $access;
|
||
private ThemeBuilderAccess $themeBuilderAccess;
|
||
private DesignSystemWriter $designWriter;
|
||
|
||
/** @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->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();
|
||
}
|
||
|
||
/**
|
||
* 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 Divi tab (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 admin check as REST route and WP-CLI command (spec §6, §7) and
|
||
* the tab "Divi" after the plugin's own tabs.
|
||
*/
|
||
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);
|
||
});
|
||
|
||
// Spec §6: the admin check runs as a REST request (same context as MCP calls). Its short form has no
|
||
// Tools-tab gate: the check tests Divi, not this site's tool switches.
|
||
$check = fn (): CompatibilityCheck => CompatibilityCheck::create($this->shortForm(null));
|
||
(new DiviCheckRoute($check))->register();
|
||
DiviCheckCommand::register($check);
|
||
|
||
add_filter(
|
||
SettingsPage::TABS_FILTER,
|
||
fn (array $tabs): array => [...$tabs, new DiviTab(log: $this->log, overview: $this->featureOverview())]
|
||
);
|
||
}
|
||
|
||
public function abilities(): array
|
||
{
|
||
$loader = $this->loader();
|
||
$writer = $this->writer($loader);
|
||
|
||
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()),
|
||
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->designWriter,
|
||
$this->designChange(),
|
||
new SnapshotStore(),
|
||
DesignRights::unfilteredHtml()
|
||
),
|
||
new DiviPresetsAssignAbility(
|
||
$this->designWriter,
|
||
$this->pageRewriter($loader),
|
||
$this->presetAssignment(...)
|
||
),
|
||
new DiviDesignDetachAbility(
|
||
$this->designWriter,
|
||
new GlobalVariablesAdapter(),
|
||
$this->designChange(),
|
||
$this->globalUsages(),
|
||
$this->pageRewriter($loader),
|
||
DesignRights::unfilteredHtml()
|
||
),
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 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 */
|
||
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);
|
||
}
|
||
}
|