feat: jedes Divi-Tool und lit/content-create erklärt seine Divi-Fähigkeiten (RequiresDiviFeatures, DeclaresRequirements, FeatureSets); Tabelle Tool → Fähigkeiten als Test
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
e387d8d29e
commit
fdbda8012c
@@ -12,7 +12,9 @@ use LIT\MCP\Abilities\Errors;
|
||||
use LIT\MCP\Abilities\ToolResult;
|
||||
use LIT\MCP\Abilities\WriteFailed;
|
||||
use LIT\MCP\Integrations\Divi\Library\GlobalUsages;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviLocked;
|
||||
use LIT\MCP\Integrations\Divi\Release\RequiresDiviFeatures;
|
||||
use LIT\MCP\Log;
|
||||
use WP_Error;
|
||||
|
||||
@@ -28,7 +30,7 @@ defined('ABSPATH') || exit;
|
||||
* anything is written (P8a) – then at most PageRewriter::PAGE_LIMIT pages. A default preset keeps working implicitly
|
||||
* (Divi applies it); only explicit references go. Variables follow with Etappe 4c.
|
||||
*/
|
||||
final class DiviDesignDetachAbility implements Ability
|
||||
final class DiviDesignDetachAbility implements Ability, RequiresDiviFeatures
|
||||
{
|
||||
private const VARIABLES = 'Variablen folgen mit Etappe 4c.';
|
||||
|
||||
@@ -113,6 +115,25 @@ final class DiviDesignDetachAbility implements Ability
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Like lit/divi-presets-assign (it rewrites every kind of post), plus the preset step that removes references
|
||||
* nested in other presets (PresetWrite).
|
||||
*/
|
||||
public function diviFeatures(array $input): array
|
||||
{
|
||||
return [
|
||||
DiviFeature::PageRead,
|
||||
DiviFeature::PageWrite,
|
||||
DiviFeature::Catalog,
|
||||
DiviFeature::DesignRead,
|
||||
DiviFeature::PresetWrite,
|
||||
DiviFeature::LibraryRead,
|
||||
DiviFeature::LibraryWrite,
|
||||
DiviFeature::ThemeBuilderWrite,
|
||||
DiviFeature::Roles,
|
||||
];
|
||||
}
|
||||
|
||||
public function permission(array $input): bool|WP_Error
|
||||
{
|
||||
return DesignRights::presets($this->writer) ?? true;
|
||||
|
||||
@@ -8,6 +8,8 @@ use Closure;
|
||||
use LIT\MCP\Abilities\Ability;
|
||||
use LIT\MCP\Abilities\Annotations;
|
||||
use LIT\MCP\Abilities\Errors;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use LIT\MCP\Integrations\Divi\Release\RequiresDiviFeatures;
|
||||
use WP_Error;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
@@ -17,7 +19,7 @@ defined('ABSPATH') || exit;
|
||||
* to the current state, confirm with design_rev, and first a snapshot of the current state, so a restore can be
|
||||
* undone as well. Etappe 4a restores presets; the other areas follow with their tools (4c, 4d).
|
||||
*/
|
||||
final class DiviDesignRestoreAbility implements Ability
|
||||
final class DiviDesignRestoreAbility implements Ability, RequiresDiviFeatures
|
||||
{
|
||||
private const ID = '/^(colors|variables|presets|theme_options)-\d{8}T\d{6}Z-[0-9a-f]{6}$/D';
|
||||
|
||||
@@ -76,6 +78,12 @@ final class DiviDesignRestoreAbility implements Ability
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Writes a stored snapshot back through the preset area (PresetWrite, reading it first: DesignRead). */
|
||||
public function diviFeatures(array $input): array
|
||||
{
|
||||
return [DiviFeature::DesignRead, DiviFeature::PresetWrite];
|
||||
}
|
||||
|
||||
public function permission(array $input): bool|WP_Error
|
||||
{
|
||||
return DesignRights::presets($this->writer) ?? true;
|
||||
|
||||
@@ -8,12 +8,14 @@ use LIT\MCP\Abilities\Ability;
|
||||
use LIT\MCP\Abilities\Annotations;
|
||||
use LIT\MCP\Abilities\Cursor;
|
||||
use LIT\MCP\Abilities\Errors;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use LIT\MCP\Integrations\Divi\Release\RequiresDiviFeatures;
|
||||
use WP_Error;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/** lit/divi-design-snapshots (spec §5): the snapshots of one design area, newest first, without their content. */
|
||||
final class DiviDesignSnapshotsAbility implements Ability
|
||||
final class DiviDesignSnapshotsAbility implements Ability, RequiresDiviFeatures
|
||||
{
|
||||
public function __construct(private SnapshotStore $snapshots)
|
||||
{
|
||||
@@ -70,6 +72,12 @@ final class DiviDesignSnapshotsAbility implements Ability
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Reads LIT MCP's own snapshot store (WordPress option); no Divi call. */
|
||||
public function diviFeatures(array $input): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function permission(array $input): bool|WP_Error
|
||||
{
|
||||
return current_user_can('edit_theme_options')
|
||||
|
||||
@@ -7,6 +7,8 @@ namespace LIT\MCP\Integrations\Divi\Design;
|
||||
use LIT\MCP\Abilities\Ability;
|
||||
use LIT\MCP\Abilities\Annotations;
|
||||
use LIT\MCP\Abilities\Errors;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use LIT\MCP\Integrations\Divi\Release\RequiresDiviFeatures;
|
||||
use WP_Error;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
@@ -16,7 +18,7 @@ defined('ABSPATH') || exit;
|
||||
* placeholder to use for each (spec §5.4, §5.7). Read live from Divi; writing follows in
|
||||
* Etappe 4.
|
||||
*/
|
||||
final class DiviDesignSystemAbility implements Ability
|
||||
final class DiviDesignSystemAbility implements Ability, RequiresDiviFeatures
|
||||
{
|
||||
/** Presets per answer (ruling S19); narrow with "module". */
|
||||
public const PRESET_LIMIT = 200;
|
||||
@@ -121,6 +123,12 @@ final class DiviDesignSystemAbility implements Ability
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Only the design system reader. */
|
||||
public function diviFeatures(array $input): array
|
||||
{
|
||||
return [DiviFeature::DesignRead];
|
||||
}
|
||||
|
||||
public function permission(array $input): bool|WP_Error
|
||||
{
|
||||
return current_user_can('edit_posts')
|
||||
|
||||
@@ -11,6 +11,8 @@ use LIT\MCP\Abilities\Errors;
|
||||
use LIT\MCP\Abilities\JsonMap;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\AttrMap;
|
||||
use LIT\MCP\Integrations\Divi\Library\GlobalUsages;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use LIT\MCP\Integrations\Divi\Release\RequiresDiviFeatures;
|
||||
use LIT\MCP\Integrations\Divi\Schema\ShortformMap;
|
||||
use WP_Error;
|
||||
|
||||
@@ -22,7 +24,7 @@ defined('ABSPATH') || exit;
|
||||
* group presets with names, the posts that use it (readable ones by title, the rest as a count) and the design_rev
|
||||
* for a following lit/divi-presets-set.
|
||||
*/
|
||||
final class DiviPresetGetAbility implements Ability
|
||||
final class DiviPresetGetAbility implements Ability, RequiresDiviFeatures
|
||||
{
|
||||
public function __construct(private DesignSystemWriter $writer, private GlobalUsages $usages)
|
||||
{
|
||||
@@ -93,6 +95,12 @@ final class DiviPresetGetAbility implements Ability
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Divi's preset inventory (DesignRead) and the option groups of modules (Catalog); usages come from the database. */
|
||||
public function diviFeatures(array $input): array
|
||||
{
|
||||
return [DiviFeature::DesignRead, DiviFeature::Catalog];
|
||||
}
|
||||
|
||||
public function permission(array $input): bool|WP_Error
|
||||
{
|
||||
return current_user_can('edit_posts')
|
||||
|
||||
@@ -10,6 +10,8 @@ use LIT\MCP\Abilities\Ability;
|
||||
use LIT\MCP\Abilities\Annotations;
|
||||
use LIT\MCP\Abilities\Errors;
|
||||
use LIT\MCP\Abilities\ToolResult;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use LIT\MCP\Integrations\Divi\Release\RequiresDiviFeatures;
|
||||
use WP_Error;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
@@ -23,7 +25,7 @@ defined('ABSPATH') || exit;
|
||||
* result ("nichts zu tun"), not confirm_required: there is nothing to confirm, and a repeated call ends cleanly.
|
||||
* null removes nothing here – lit/divi-design-detach does.
|
||||
*/
|
||||
final class DiviPresetsAssignAbility implements Ability
|
||||
final class DiviPresetsAssignAbility implements Ability, RequiresDiviFeatures
|
||||
{
|
||||
/** Path prefix for PresetAssignment's errors; removed again so fields name the input keys. */
|
||||
private const ROOT = 'input';
|
||||
@@ -116,6 +118,28 @@ final class DiviPresetsAssignAbility implements Ability
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewrites references in every post that uses the module, so the upper bound covers every kind of post the
|
||||
* rewriter may save: pages (PageRead, PageWrite, Catalog, DesignRead), library entries (LibraryRead for
|
||||
* isGlobal(), LibraryWrite) and Theme Builder layouts (ThemeBuilderWrite), with their roles. Stopping in the
|
||||
* middle of a run would leave a partial write, so the lock comes before. DesignRights::presets() checks the
|
||||
* Visual Builder role through the preset writer (PresetWrite).
|
||||
*/
|
||||
public function diviFeatures(array $input): array
|
||||
{
|
||||
return [
|
||||
DiviFeature::PageRead,
|
||||
DiviFeature::PageWrite,
|
||||
DiviFeature::Catalog,
|
||||
DiviFeature::DesignRead,
|
||||
DiviFeature::PresetWrite,
|
||||
DiviFeature::LibraryRead,
|
||||
DiviFeature::LibraryWrite,
|
||||
DiviFeature::ThemeBuilderWrite,
|
||||
DiviFeature::Roles,
|
||||
];
|
||||
}
|
||||
|
||||
public function permission(array $input): bool|WP_Error
|
||||
{
|
||||
return DesignRights::presets($this->writer) ?? true;
|
||||
|
||||
@@ -10,6 +10,8 @@ use LIT\MCP\Abilities\Ability;
|
||||
use LIT\MCP\Abilities\Annotations;
|
||||
use LIT\MCP\Abilities\Errors;
|
||||
use LIT\MCP\Integrations\Divi\Library\GlobalUsages;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use LIT\MCP\Integrations\Divi\Release\RequiresDiviFeatures;
|
||||
use WP_Error;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
@@ -21,7 +23,7 @@ defined('ABSPATH') || exit;
|
||||
* block: a default preset loses its default, and the posts with modules that use it implicitly are listed in the dry
|
||||
* run (spec §6.4). Through the change pipeline with Divi's delete semantics (legacy option cleaned).
|
||||
*/
|
||||
final class DiviPresetsDeleteAbility implements Ability
|
||||
final class DiviPresetsDeleteAbility implements Ability, RequiresDiviFeatures
|
||||
{
|
||||
public const MAX_ITEMS = 20;
|
||||
|
||||
@@ -97,6 +99,12 @@ final class DiviPresetsDeleteAbility implements Ability
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Reads and writes the preset inventory only; no composition. */
|
||||
public function diviFeatures(array $input): array
|
||||
{
|
||||
return [DiviFeature::DesignRead, DiviFeature::PresetWrite];
|
||||
}
|
||||
|
||||
public function permission(array $input): bool|WP_Error
|
||||
{
|
||||
return DesignRights::presets($this->writer) ?? true;
|
||||
|
||||
@@ -12,6 +12,9 @@ use LIT\MCP\Abilities\Errors;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\Block;
|
||||
use LIT\MCP\Integrations\Divi\Library\GlobalUsages;
|
||||
use LIT\MCP\Integrations\Divi\Pages\DiviPageLoader;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use LIT\MCP\Integrations\Divi\Release\FeatureSets;
|
||||
use LIT\MCP\Integrations\Divi\Release\RequiresDiviFeatures;
|
||||
use WP_Error;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
@@ -23,7 +26,7 @@ defined('ABSPATH') || exit;
|
||||
* module/group (A5). The dry run lists the posts that use a changed preset or reach a new default, also implicitly
|
||||
* (ruling R11).
|
||||
*/
|
||||
final class DiviPresetsSetAbility implements Ability
|
||||
final class DiviPresetsSetAbility implements Ability, RequiresDiviFeatures
|
||||
{
|
||||
public const MAX_ITEMS = 20;
|
||||
|
||||
@@ -121,6 +124,27 @@ final class DiviPresetsSetAbility implements Ability
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the preset inventory (PresetWrite) after reading it (DesignRead); the preset composer cleans the
|
||||
* attributes (Catalog). from_node reads one node of an entry (PageRead, and the roles for a library entry).
|
||||
*/
|
||||
public function diviFeatures(array $input): array
|
||||
{
|
||||
$features = [DiviFeature::DesignRead, DiviFeature::PresetWrite, DiviFeature::Catalog];
|
||||
foreach (is_array($input['items'] ?? null) ? $input['items'] : [] as $item) {
|
||||
$source = is_array($item) ? ($item['from_node'] ?? null) : null;
|
||||
if (is_array($source)) {
|
||||
$features = FeatureSets::merge(
|
||||
$features,
|
||||
[DiviFeature::PageRead],
|
||||
FeatureSets::ofEntry($source['post_id'] ?? null, false)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $features;
|
||||
}
|
||||
|
||||
public function permission(array $input): bool|WP_Error
|
||||
{
|
||||
return DesignRights::presets($this->writer) ?? true;
|
||||
|
||||
@@ -18,6 +18,8 @@ use LIT\MCP\Integrations\Divi\BlockTree\NodeSummary;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\Parser;
|
||||
use LIT\MCP\Integrations\Divi\Pages\DiviErrors;
|
||||
use LIT\MCP\Integrations\Divi\Pages\DiviPageLoader;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use LIT\MCP\Integrations\Divi\Release\RequiresDiviFeatures;
|
||||
use LIT\MCP\Integrations\Divi\ThemeBuilder\DiviThemeBuilder;
|
||||
use LIT\MCP\Integrations\WordPress\PostGuard;
|
||||
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
|
||||
@@ -37,7 +39,7 @@ defined('ABSPATH') || exit;
|
||||
* and next_cursor resumes behind it. Needs the library right (canList); a hidden library answers its entries like
|
||||
* missing IDs (LibraryAccess::item, P9/P12). Only hashes and the matched nodes' labels are returned.
|
||||
*/
|
||||
final class LibraryFindSimilarAbility implements Ability
|
||||
final class LibraryFindSimilarAbility implements Ability, RequiresDiviFeatures
|
||||
{
|
||||
public const MAX_POSTS = 200;
|
||||
public const DEFAULT_POSTS = 100;
|
||||
@@ -121,6 +123,15 @@ final class LibraryFindSimilarAbility implements Ability
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans Divi pages (PageRead) of the types that use the builder (Catalog), after the library right (Roles). A
|
||||
* library entry as reference is read through LibraryAccess (Roles), no Divi call beyond that.
|
||||
*/
|
||||
public function diviFeatures(array $input): array
|
||||
{
|
||||
return [DiviFeature::PageRead, DiviFeature::Catalog, DiviFeature::Roles];
|
||||
}
|
||||
|
||||
public function permission(array $input): bool|WP_Error
|
||||
{
|
||||
return $this->access->canList() ?? true;
|
||||
|
||||
@@ -19,6 +19,7 @@ use LIT\MCP\Integrations\Divi\Pages\DiviPage;
|
||||
use LIT\MCP\Integrations\Divi\Pages\DiviSchema;
|
||||
use LIT\MCP\Integrations\Divi\Pages\DiviWriteAbility;
|
||||
use LIT\MCP\Integrations\Divi\Pages\DiviWriter;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use LIT\MCP\Integrations\WordPress\PostGuard;
|
||||
use WP_Error;
|
||||
use WP_Post;
|
||||
@@ -103,6 +104,23 @@ final class LibraryInsertAbility extends DiviWriteAbility
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Like lit/divi-insert (StoredNodeGuard cleans the entry's nodes: Catalog, DesignRead, DynamicSources), plus the
|
||||
* library entry as source: isGlobal() is a Divi call (LibraryRead), its rights are a Divi role (Roles).
|
||||
*/
|
||||
protected function baseFeatures(): array
|
||||
{
|
||||
return [
|
||||
DiviFeature::PageRead,
|
||||
DiviFeature::PageWrite,
|
||||
DiviFeature::Catalog,
|
||||
DiviFeature::DesignRead,
|
||||
DiviFeature::DynamicSources,
|
||||
DiviFeature::LibraryRead,
|
||||
DiviFeature::Roles,
|
||||
];
|
||||
}
|
||||
|
||||
protected function change(DiviPage $page, array $input, DiviComposition $composition): DiviChange|WP_Error
|
||||
{
|
||||
$mode = $input['mode'] ?? null;
|
||||
|
||||
@@ -13,6 +13,8 @@ use LIT\MCP\Integrations\Divi\BlockTree\Outline;
|
||||
use LIT\MCP\Integrations\Divi\Design\DesignSystemReader;
|
||||
use LIT\MCP\Integrations\Divi\Pages\DiviPage;
|
||||
use LIT\MCP\Integrations\Divi\Pages\DiviPageLoader;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use LIT\MCP\Integrations\Divi\Release\RequiresDiviFeatures;
|
||||
use LIT\MCP\Integrations\WordPress\PostGuard;
|
||||
use WP_Error;
|
||||
use WP_Post;
|
||||
@@ -27,7 +29,7 @@ defined('ABSPATH') || exit;
|
||||
* left out, without title or count). With id,
|
||||
* everything but a library entry the user may read answers like a missing ID (ruling P12).
|
||||
*/
|
||||
final class LibraryListAbility implements Ability
|
||||
final class LibraryListAbility implements Ability, RequiresDiviFeatures
|
||||
{
|
||||
/** Rows looked at per call beyond the entries already shown (the cursor), skipping hidden ones. */
|
||||
public const MAX_SCAN = 500;
|
||||
@@ -121,6 +123,19 @@ final class LibraryListAbility implements Ability
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* describe() asks Divi whether an entry is global (LibraryRead); listing needs the library role (Roles). One
|
||||
* entry (id) is loaded and outlined (PageRead, DesignRead).
|
||||
*/
|
||||
public function diviFeatures(array $input): array
|
||||
{
|
||||
$features = [DiviFeature::LibraryRead, DiviFeature::Roles];
|
||||
|
||||
return isset($input['id'])
|
||||
? array_merge($features, [DiviFeature::PageRead, DiviFeature::DesignRead])
|
||||
: $features;
|
||||
}
|
||||
|
||||
public function permission(array $input): bool|WP_Error
|
||||
{
|
||||
if (isset($input['id'])) {
|
||||
|
||||
@@ -23,6 +23,8 @@ use LIT\MCP\Integrations\Divi\Pages\DiviPage;
|
||||
use LIT\MCP\Integrations\Divi\Pages\DiviSchema;
|
||||
use LIT\MCP\Integrations\Divi\Pages\DiviWriter;
|
||||
use LIT\MCP\Integrations\Divi\Pages\VisibleEdits;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use LIT\MCP\Integrations\Divi\Release\RequiresDiviFeatures;
|
||||
use LIT\MCP\Integrations\WordPress\Content\NewDraft;
|
||||
use LIT\MCP\Integrations\WordPress\PostGuard;
|
||||
use LIT\MCP\Integrations\WordPress\PostSaveFollowUp;
|
||||
@@ -59,7 +61,7 @@ defined('ABSPATH') || exit;
|
||||
* point), page N is rolled back by DiviSaver (or reported partial), pages after N are not attempted – the error
|
||||
* names all three groups (changed_posts with pages, failed_post, unchanged_posts; D10, S10).
|
||||
*/
|
||||
final class LibraryMakeGlobalAbility implements Ability
|
||||
final class LibraryMakeGlobalAbility implements Ability, RequiresDiviFeatures
|
||||
{
|
||||
public const REASON_PAGE = 'Die Seite konnte nicht gespeichert werden; der neue globale Eintrag wurde wieder '
|
||||
. 'entfernt.';
|
||||
@@ -176,6 +178,25 @@ final class LibraryMakeGlobalAbility implements Ability
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Both modes write pages and posts (PageRead, PageWrite, Catalog, DesignRead, DynamicSources) and ask Divi
|
||||
* whether an entry is global (LibraryRead); the first mode also creates the global entry (LibraryWrite).
|
||||
* One upper bound for both modes (the second mode would not reach LibraryWrite): the roles of the library.
|
||||
*/
|
||||
public function diviFeatures(array $input): array
|
||||
{
|
||||
return [
|
||||
DiviFeature::PageRead,
|
||||
DiviFeature::PageWrite,
|
||||
DiviFeature::Catalog,
|
||||
DiviFeature::DesignRead,
|
||||
DiviFeature::DynamicSources,
|
||||
DiviFeature::LibraryRead,
|
||||
DiviFeature::LibraryWrite,
|
||||
DiviFeature::Roles,
|
||||
];
|
||||
}
|
||||
|
||||
public function permission(array $input): bool|WP_Error
|
||||
{
|
||||
return $this->precheck($input) ?? true;
|
||||
|
||||
@@ -17,6 +17,8 @@ use LIT\MCP\Integrations\Divi\Pages\DiviPage;
|
||||
use LIT\MCP\Integrations\Divi\Pages\DiviPageLoader;
|
||||
use LIT\MCP\Integrations\Divi\Pages\DiviSaver;
|
||||
use LIT\MCP\Integrations\Divi\Pages\DiviWriter;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use LIT\MCP\Integrations\Divi\Release\RequiresDiviFeatures;
|
||||
use LIT\MCP\Integrations\Divi\Schema\DiviModules;
|
||||
use LIT\MCP\Integrations\WordPress\Content\NewDraft;
|
||||
use LIT\MCP\Integrations\WordPress\PostSaveFollowUp;
|
||||
@@ -35,7 +37,7 @@ defined('ABSPATH') || exit;
|
||||
* to the entry is lit/library-make-global. All or nothing through NewDraft (ruling P6): any failure
|
||||
* after the insert deletes the new entry, a failed delete is an honest partial_write.
|
||||
*/
|
||||
final class LibrarySaveAbility implements Ability
|
||||
final class LibrarySaveAbility implements Ability, RequiresDiviFeatures
|
||||
{
|
||||
private const TYPE_LABELS = ['section' => 'Abschnitt', 'row' => 'Zeile', 'module' => 'Modul'];
|
||||
|
||||
@@ -112,6 +114,24 @@ final class LibrarySaveAbility implements Ability
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a node of any entry (PageRead), cleans it with StoredNodeGuard (Catalog, DesignRead, DynamicSources) and
|
||||
* saves a new library entry (PageWrite and LibraryWrite: DiviSaver::saveNew with the library scope, D5), after
|
||||
* the library right (Roles). The source is only read: a library entry or Theme Builder layout adds Roles only.
|
||||
*/
|
||||
public function diviFeatures(array $input): array
|
||||
{
|
||||
return [
|
||||
DiviFeature::PageRead,
|
||||
DiviFeature::PageWrite,
|
||||
DiviFeature::Catalog,
|
||||
DiviFeature::DesignRead,
|
||||
DiviFeature::DynamicSources,
|
||||
DiviFeature::LibraryWrite,
|
||||
DiviFeature::Roles,
|
||||
];
|
||||
}
|
||||
|
||||
public function permission(array $input): bool|WP_Error
|
||||
{
|
||||
$refusal = $this->access->canSave(self::isGlobal($input));
|
||||
|
||||
@@ -10,6 +10,7 @@ use LIT\MCP\Integrations\Divi\BlockTree\NodeIndex;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\Outline;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\Serializer;
|
||||
use LIT\MCP\Integrations\Divi\Design\DesignSystemReader;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
|
||||
use LIT\MCP\Integrations\Divi\Schema\DiviModules;
|
||||
use LIT\MCP\Integrations\WordPress\Content\ContentComposer;
|
||||
@@ -36,6 +37,19 @@ defined('ABSPATH') || exit;
|
||||
*/
|
||||
final class DiviContentComposer implements ContentComposer
|
||||
{
|
||||
/**
|
||||
* What a new Divi entry needs: Catalog (diviEnabled(), composing), DesignRead (presets, variables), DynamicSources
|
||||
* (the composition checks references against the new entry's map), PageRead (link targets) and PageWrite (saveNew
|
||||
* with the Divi flags). No library or Theme Builder scope: the draft is an ordinary entry.
|
||||
*/
|
||||
private const FEATURES = [
|
||||
DiviFeature::PageRead,
|
||||
DiviFeature::PageWrite,
|
||||
DiviFeature::Catalog,
|
||||
DiviFeature::DesignRead,
|
||||
DiviFeature::DynamicSources,
|
||||
];
|
||||
|
||||
/** @var Closure(): bool */
|
||||
private Closure $writable;
|
||||
|
||||
@@ -60,6 +74,11 @@ final class DiviContentComposer implements ContentComposer
|
||||
return ($this->writable)();
|
||||
}
|
||||
|
||||
public function requirements(): array
|
||||
{
|
||||
return array_map(static fn (DiviFeature $feature): string => $feature->value, self::FEATURES);
|
||||
}
|
||||
|
||||
public function refusal(): ?WP_Error
|
||||
{
|
||||
$status = ReleaseGate::status();
|
||||
|
||||
@@ -8,6 +8,7 @@ use LIT\MCP\Abilities\Annotations;
|
||||
use LIT\MCP\Abilities\Errors;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\Block;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\NodeSummary;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use WP_Error;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
@@ -60,6 +61,12 @@ final class DiviDeleteAbility extends DiviWriteAbility
|
||||
return Annotations::destructive(false);
|
||||
}
|
||||
|
||||
/** Like lit/divi-move: TreeEditor::remove() only, no composition of client values. */
|
||||
protected function baseFeatures(): array
|
||||
{
|
||||
return [DiviFeature::PageRead, DiviFeature::PageWrite, DiviFeature::Catalog, DiviFeature::DesignRead];
|
||||
}
|
||||
|
||||
protected function change(DiviPage $page, array $input, DiviComposition $composition): DiviChange|WP_Error
|
||||
{
|
||||
/** @var array<string, Block> $nodes */
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace LIT\MCP\Integrations\Divi\Pages;
|
||||
|
||||
use LIT\MCP\Abilities\Annotations;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\AttrMap;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use WP_Error;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
@@ -55,6 +56,12 @@ final class DiviDuplicateNodeAbility extends DiviWriteAbility
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Like lit/divi-move: TreeEditor::duplicate() copies stored bytes, no composition of client values. */
|
||||
protected function baseFeatures(): array
|
||||
{
|
||||
return [DiviFeature::PageRead, DiviFeature::PageWrite, DiviFeature::Catalog, DiviFeature::DesignRead];
|
||||
}
|
||||
|
||||
protected function change(DiviPage $page, array $input, DiviComposition $composition): DiviChange|WP_Error
|
||||
{
|
||||
$id = is_string($input['id'] ?? null) ? $input['id'] : '';
|
||||
|
||||
@@ -11,6 +11,8 @@ use LIT\MCP\Abilities\JsonMap;
|
||||
use LIT\MCP\Integrations\Divi\Compose\DynamicContentRules;
|
||||
use LIT\MCP\Integrations\Divi\Compose\Placeholders;
|
||||
use LIT\MCP\Integrations\Divi\Library\DiviRoles;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use LIT\MCP\Integrations\Divi\Release\RequiresDiviFeatures;
|
||||
use LIT\MCP\Integrations\WordPress\PostGuard;
|
||||
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
|
||||
use WP_Error;
|
||||
@@ -28,7 +30,7 @@ defined('ABSPATH') || exit;
|
||||
* left out with its reason (DynamicFields). Hidden fields (security audit M4) are neither offered nor
|
||||
* named. The field list needs the Divi role option read_dynamic_content_custom_fields (E3, F25).
|
||||
*/
|
||||
final class DiviDynamicSourcesAbility implements Ability
|
||||
final class DiviDynamicSourcesAbility implements Ability, RequiresDiviFeatures
|
||||
{
|
||||
/** Candidates the default sample lookup checks for edit_post. */
|
||||
private const SAMPLE_CANDIDATES = 10;
|
||||
@@ -130,6 +132,15 @@ final class DiviDynamicSourcesAbility implements Ability
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Divi's dynamic content registry (DynamicSources); listing custom fields asks the role option
|
||||
* read_dynamic_content_custom_fields (Roles) and omits them without it, so a broken roles check must not pass.
|
||||
*/
|
||||
public function diviFeatures(array $input): array
|
||||
{
|
||||
return [DiviFeature::DynamicSources, DiviFeature::Roles];
|
||||
}
|
||||
|
||||
public function permission(array $input): bool|WP_Error
|
||||
{
|
||||
$type = PostGuard::postType($input['post_type'] ?? null, $this->schema, 'edit_posts');
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace LIT\MCP\Integrations\Divi\Pages;
|
||||
|
||||
use LIT\MCP\Abilities\Annotations;
|
||||
use LIT\MCP\Abilities\Errors;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use WP_Error;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
@@ -64,6 +65,21 @@ final class DiviInsertAbility extends DiviWriteAbility
|
||||
return Annotations::write(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads and saves the entry (PageRead, PageWrite) and composes new nodes: Catalog, DesignRead (presets,
|
||||
* variables) and DynamicSources (references are checked against the dynamic content map of the entry).
|
||||
*/
|
||||
protected function baseFeatures(): array
|
||||
{
|
||||
return [
|
||||
DiviFeature::PageRead,
|
||||
DiviFeature::PageWrite,
|
||||
DiviFeature::Catalog,
|
||||
DiviFeature::DesignRead,
|
||||
DiviFeature::DynamicSources,
|
||||
];
|
||||
}
|
||||
|
||||
protected function change(DiviPage $page, array $input, DiviComposition $composition): DiviChange|WP_Error
|
||||
{
|
||||
$editor = $composition->editor();
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace LIT\MCP\Integrations\Divi\Pages;
|
||||
|
||||
use LIT\MCP\Abilities\Annotations;
|
||||
use LIT\MCP\Abilities\Errors;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use WP_Error;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
@@ -52,6 +53,15 @@ final class DiviMoveAbility extends DiviWriteAbility
|
||||
return Annotations::write(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* No client value enters the tree (no DynamicSources): load and save (PageRead, PageWrite), StructureRules
|
||||
* need the catalog, the quality warnings of the result read the design system (DesignRead).
|
||||
*/
|
||||
protected function baseFeatures(): array
|
||||
{
|
||||
return [DiviFeature::PageRead, DiviFeature::PageWrite, DiviFeature::Catalog, DiviFeature::DesignRead];
|
||||
}
|
||||
|
||||
protected function change(DiviPage $page, array $input, DiviComposition $composition): DiviChange|WP_Error
|
||||
{
|
||||
$id = is_string($input['id'] ?? null) ? $input['id'] : '';
|
||||
|
||||
@@ -7,6 +7,8 @@ namespace LIT\MCP\Integrations\Divi\Pages;
|
||||
use Closure;
|
||||
use LIT\MCP\Abilities\Annotations;
|
||||
use LIT\MCP\Abilities\Errors;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use LIT\MCP\Integrations\Divi\Release\FeatureSets;
|
||||
use WP_Error;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
@@ -117,6 +119,29 @@ final class DiviNodeCopyAbility extends DiviWriteAbility
|
||||
return $this->loader->library()?->canInsert();
|
||||
}
|
||||
|
||||
/**
|
||||
* Like lit/divi-insert (StoredNodeGuard cleans the copied node: Catalog, DesignRead, DynamicSources); the source
|
||||
* entry is only read, so a library entry or Theme Builder layout as source adds the Divi roles.
|
||||
*/
|
||||
protected function baseFeatures(): array
|
||||
{
|
||||
return [
|
||||
DiviFeature::PageRead,
|
||||
DiviFeature::PageWrite,
|
||||
DiviFeature::Catalog,
|
||||
DiviFeature::DesignRead,
|
||||
DiviFeature::DynamicSources,
|
||||
];
|
||||
}
|
||||
|
||||
public function diviFeatures(array $input): array
|
||||
{
|
||||
return FeatureSets::merge(
|
||||
parent::diviFeatures($input),
|
||||
FeatureSets::ofEntry($input['source_post_id'] ?? null, false)
|
||||
);
|
||||
}
|
||||
|
||||
protected function change(DiviPage $page, array $input, DiviComposition $composition): DiviChange|WP_Error
|
||||
{
|
||||
$source = $this->loader->load($input['source_post_id'] ?? null, false);
|
||||
|
||||
@@ -14,6 +14,9 @@ use LIT\MCP\Integrations\Divi\BlockTree\Block;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\NodeIndex;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\NodeSummary;
|
||||
use LIT\MCP\Integrations\Divi\Design\DesignSystemReader;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use LIT\MCP\Integrations\Divi\Release\FeatureSets;
|
||||
use LIT\MCP\Integrations\Divi\Release\RequiresDiviFeatures;
|
||||
use LIT\MCP\Integrations\Divi\Schema\ShortformMap;
|
||||
use WP_Error;
|
||||
|
||||
@@ -23,7 +26,7 @@ defined('ABSPATH') || exit;
|
||||
* lit/divi-node-get: all attributes of one node in the full form (spec §5.4, §5.7) –
|
||||
* attribute path => breakpoint map – plus the current short-form values.
|
||||
*/
|
||||
final class DiviNodeGetAbility implements Ability
|
||||
final class DiviNodeGetAbility implements Ability, RequiresDiviFeatures
|
||||
{
|
||||
public function __construct(private DiviPageLoader $loader, private DesignSystemReader $design)
|
||||
{
|
||||
@@ -123,6 +126,18 @@ final class DiviNodeGetAbility implements Ability
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the entry (PageRead) and names preset labels (DesignRead); ShortformMap is pure. A library entry or
|
||||
* Theme Builder layout as post_id adds the Divi roles.
|
||||
*/
|
||||
public function diviFeatures(array $input): array
|
||||
{
|
||||
return FeatureSets::merge(
|
||||
[DiviFeature::PageRead, DiviFeature::DesignRead],
|
||||
FeatureSets::ofEntry($input['post_id'] ?? null, false)
|
||||
);
|
||||
}
|
||||
|
||||
public function permission(array $input): bool|WP_Error
|
||||
{
|
||||
$page = $this->loader->load($input['post_id'] ?? null, false);
|
||||
|
||||
@@ -11,6 +11,9 @@ use LIT\MCP\Abilities\Rev;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\NodeIndex;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\Outline;
|
||||
use LIT\MCP\Integrations\Divi\Design\DesignSystemReader;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use LIT\MCP\Integrations\Divi\Release\FeatureSets;
|
||||
use LIT\MCP\Integrations\Divi\Release\RequiresDiviFeatures;
|
||||
use WP_Error;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
@@ -19,7 +22,7 @@ defined('ABSPATH') || exit;
|
||||
* lit/divi-outline: the page tree with path IDs, types, admin labels, text excerpts and
|
||||
* preset names plus tree_rev (spec §5.3, §5.7). Every Divi write needs this tree_rev.
|
||||
*/
|
||||
final class DiviOutlineAbility implements Ability
|
||||
final class DiviOutlineAbility implements Ability, RequiresDiviFeatures
|
||||
{
|
||||
public function __construct(private DiviPageLoader $loader, private DesignSystemReader $design)
|
||||
{
|
||||
@@ -137,6 +140,18 @@ final class DiviOutlineAbility implements Ability
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the entry (PageRead) and names preset labels (DesignRead). Global modules are shown by ID only, never
|
||||
* resolved. A library entry or Theme Builder layout as post_id adds the Divi roles.
|
||||
*/
|
||||
public function diviFeatures(array $input): array
|
||||
{
|
||||
return FeatureSets::merge(
|
||||
[DiviFeature::PageRead, DiviFeature::DesignRead],
|
||||
FeatureSets::ofEntry($input['post_id'] ?? null, false)
|
||||
);
|
||||
}
|
||||
|
||||
public function permission(array $input): bool|WP_Error
|
||||
{
|
||||
$page = $this->loader->load($input['post_id'] ?? null, false);
|
||||
|
||||
@@ -13,6 +13,9 @@ use LIT\MCP\Abilities\ToolResult;
|
||||
use LIT\MCP\Abilities\WriteFailed;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\Block;
|
||||
use LIT\MCP\Integrations\Divi\Compose\TextReplacer;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use LIT\MCP\Integrations\Divi\Release\FeatureSets;
|
||||
use LIT\MCP\Integrations\Divi\Release\RequiresDiviFeatures;
|
||||
use LIT\MCP\Integrations\WordPress\PostGuard;
|
||||
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
|
||||
use LIT\MCP\Log;
|
||||
@@ -36,7 +39,7 @@ defined('ABSPATH') || exit;
|
||||
* others go on. Inserted text is cleaned by the field kind through
|
||||
* ValueCleaner::textReplacementFor() (S2/S28); URL and code fields are never touched.
|
||||
*/
|
||||
final class DiviReplaceTextAbility implements Ability
|
||||
final class DiviReplaceTextAbility implements Ability, RequiresDiviFeatures
|
||||
{
|
||||
private const MAX_TARGETS = 20;
|
||||
|
||||
@@ -128,6 +131,34 @@ final class DiviReplaceTextAbility implements Ability
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* The dry run (default) reads: PageRead, Catalog and DesignRead; the cleaner builds its reference check from the
|
||||
* entry's dynamic content map (DynamicSources). A real run (dry_run: false) also saves each target, which may be a
|
||||
* library entry or Theme Builder layout (PageWrite and what FeatureSets::ofEntry() adds).
|
||||
*/
|
||||
public function diviFeatures(array $input): array
|
||||
{
|
||||
$features = [
|
||||
DiviFeature::PageRead,
|
||||
DiviFeature::Catalog,
|
||||
DiviFeature::DesignRead,
|
||||
DiviFeature::DynamicSources,
|
||||
];
|
||||
if (($input['dry_run'] ?? true) !== false) {
|
||||
return $features;
|
||||
}
|
||||
|
||||
$features[] = DiviFeature::PageWrite;
|
||||
foreach (is_array($input['targets'] ?? null) ? $input['targets'] : [] as $target) {
|
||||
$features = FeatureSets::merge(
|
||||
$features,
|
||||
FeatureSets::ofEntry(is_array($target) ? ($target['post_id'] ?? null) : null, true)
|
||||
);
|
||||
}
|
||||
|
||||
return $features;
|
||||
}
|
||||
|
||||
public function permission(array $input): bool|WP_Error
|
||||
{
|
||||
$type = PostGuard::postType($input['post_type'] ?? 'page', $this->schema, 'edit_posts');
|
||||
|
||||
@@ -9,6 +9,7 @@ use LIT\MCP\Abilities\Errors;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\Block;
|
||||
use LIT\MCP\Integrations\Divi\Compose\ColumnWidths;
|
||||
use LIT\MCP\Integrations\Divi\Design\PresetAssignment;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use WP_Error;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
@@ -101,6 +102,18 @@ final class DiviUpdateAbility extends DiviWriteAbility
|
||||
return Annotations::write(true);
|
||||
}
|
||||
|
||||
/** Like lit/divi-insert: the Composer checks every changed value (Catalog, DesignRead, DynamicSources). */
|
||||
protected function baseFeatures(): array
|
||||
{
|
||||
return [
|
||||
DiviFeature::PageRead,
|
||||
DiviFeature::PageWrite,
|
||||
DiviFeature::Catalog,
|
||||
DiviFeature::DesignRead,
|
||||
DiviFeature::DynamicSources,
|
||||
];
|
||||
}
|
||||
|
||||
protected function change(DiviPage $page, array $input, DiviComposition $composition): DiviChange|WP_Error
|
||||
{
|
||||
$changes = is_array($input['changes'] ?? null) ? array_values($input['changes']) : [];
|
||||
|
||||
@@ -6,6 +6,9 @@ namespace LIT\MCP\Integrations\Divi\Pages;
|
||||
|
||||
use LIT\MCP\Abilities\Ability;
|
||||
use LIT\MCP\Abilities\ToolResult;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use LIT\MCP\Integrations\Divi\Release\FeatureSets;
|
||||
use LIT\MCP\Integrations\Divi\Release\RequiresDiviFeatures;
|
||||
use WP_Error;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
@@ -17,7 +20,7 @@ defined('ABSPATH') || exit;
|
||||
* validated, all or nothing (ruling S39) – and DiviWriter::commit() stores it (library entries:
|
||||
* GlobalEdits) and builds the result. The composition is bound to the page (DiviWriter::composition(), ruling S22).
|
||||
*/
|
||||
abstract class DiviWriteAbility implements Ability
|
||||
abstract class DiviWriteAbility implements Ability, RequiresDiviFeatures
|
||||
{
|
||||
public function __construct(protected DiviWriter $writer)
|
||||
{
|
||||
@@ -31,6 +34,19 @@ abstract class DiviWriteAbility implements Ability
|
||||
*/
|
||||
abstract protected function change(DiviPage $page, array $input, DiviComposition $composition): DiviChange|WP_Error;
|
||||
|
||||
/**
|
||||
* What the tool needs whatever entry it changes; diviFeatures() adds what the entry post_id names (a library
|
||||
* entry or Theme Builder layout is saved with its own capability, D5).
|
||||
*
|
||||
* @return list<DiviFeature>
|
||||
*/
|
||||
abstract protected function baseFeatures(): array;
|
||||
|
||||
public function diviFeatures(array $input): array
|
||||
{
|
||||
return FeatureSets::merge($this->baseFeatures(), FeatureSets::ofEntry($input['post_id'] ?? null, true));
|
||||
}
|
||||
|
||||
public function outputSchema(): ?array
|
||||
{
|
||||
return ToolResult::schema([
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Release;
|
||||
|
||||
use LIT\MCP\Abilities\Ability;
|
||||
use LIT\MCP\Abilities\DeclaresRequirements;
|
||||
use LIT\MCP\Integrations\Divi\Library\DiviLibraryStore;
|
||||
use LIT\MCP\Integrations\Divi\ThemeBuilder\DiviThemeBuilder;
|
||||
use LIT\MCP\Integrations\WordPress\PostGuard;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/** Helpers of RequiresDiviFeatures::diviFeatures(): joining lists and the capabilities an entry adds. */
|
||||
final class FeatureSets
|
||||
{
|
||||
/**
|
||||
* The capabilities $ability declares for $input: its own diviFeatures() (the Divi tools) or the IDs a core tool
|
||||
* names through DeclaresRequirements (lit/content-create with divi). Anything else declares none; an ID that is no
|
||||
* DiviFeature is ignored (the core may name capabilities of other integrations).
|
||||
*
|
||||
* @param array<string, mixed> $input
|
||||
* @return list<DiviFeature>
|
||||
*/
|
||||
public static function declaredBy(Ability $ability, array $input): array
|
||||
{
|
||||
if ($ability instanceof RequiresDiviFeatures) {
|
||||
return self::merge($ability->diviFeatures($input));
|
||||
}
|
||||
|
||||
if (! $ability instanceof DeclaresRequirements) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$features = [];
|
||||
foreach ($ability->requirements($input) as $id) {
|
||||
$feature = DiviFeature::tryFrom($id);
|
||||
if ($feature !== null) {
|
||||
$features[] = $feature;
|
||||
}
|
||||
}
|
||||
|
||||
return self::merge($features);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<DiviFeature> ...$lists
|
||||
* @return list<DiviFeature> Every capability once, in the order of first appearance.
|
||||
*/
|
||||
public static function merge(array ...$lists): array
|
||||
{
|
||||
$merged = [];
|
||||
foreach ($lists as $list) {
|
||||
foreach ($list as $feature) {
|
||||
$merged[$feature->value] ??= $feature;
|
||||
}
|
||||
}
|
||||
|
||||
return array_values($merged);
|
||||
}
|
||||
|
||||
/**
|
||||
* What the entry $postId adds to a tool that loads it. A page or post adds nothing; a library entry needs the
|
||||
* Divi roles (LibraryAccess), written also the library check of global entries and the library save scope
|
||||
* (DiviSaver, D5); a Theme Builder layout needs the roles, written also the template lookup of
|
||||
* ThemeBuilderEdits and the Theme Builder save scope. An ID that names no entry, or one the caller may not edit,
|
||||
* adds nothing – the tool answers "not found" or "forbidden" itself, and a lock never tells which kind of entry
|
||||
* an ID is (ruling P12).
|
||||
*
|
||||
* @return list<DiviFeature>
|
||||
*/
|
||||
public static function ofEntry(mixed $postId, bool $write): array
|
||||
{
|
||||
$id = PostGuard::id($postId);
|
||||
$type = $id === null ? false : get_post_type($id);
|
||||
if ($id === null || ! is_string($type)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$isLibrary = $type === DiviLibraryStore::POST_TYPE;
|
||||
if (! $isLibrary && DiviThemeBuilder::areaOf($type) === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (! current_user_can('edit_post', $id)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if ($isLibrary) {
|
||||
return $write
|
||||
? [DiviFeature::Roles, DiviFeature::LibraryRead, DiviFeature::LibraryWrite]
|
||||
: [DiviFeature::Roles];
|
||||
}
|
||||
|
||||
return $write
|
||||
? [DiviFeature::Roles, DiviFeature::ThemeBuilderRead, DiviFeature::ThemeBuilderWrite]
|
||||
: [DiviFeature::Roles];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Release;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* A tool that says which Divi capabilities a call needs. DiviFeatureGate refuses the call before it runs while one of
|
||||
* them is not usable; the adapters still check each write themselves (ReleaseGate::assertFeature()).
|
||||
*/
|
||||
interface RequiresDiviFeatures
|
||||
{
|
||||
/**
|
||||
* The capabilities this call needs for $input, the upper bound of what the call may reach: a capability an
|
||||
* adapter checks later must be in it, or the tool would stop in the middle of its work instead of before.
|
||||
* [] = none (a tool that never touches Divi).
|
||||
*
|
||||
* @param array<string, mixed> $input
|
||||
* @return list<DiviFeature>
|
||||
*/
|
||||
public function diviFeatures(array $input): array;
|
||||
}
|
||||
@@ -8,6 +8,8 @@ use LIT\MCP\Abilities\Ability;
|
||||
use LIT\MCP\Abilities\Annotations;
|
||||
use LIT\MCP\Abilities\Errors;
|
||||
use LIT\MCP\Abilities\JsonMap;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use LIT\MCP\Integrations\Divi\Release\RequiresDiviFeatures;
|
||||
use WP_Error;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
@@ -16,7 +18,7 @@ defined('ABSPATH') || exit;
|
||||
* lit/divi-catalog: the modules of this Divi installation with their short-form fields;
|
||||
* with "module" the details of one module incl. all known attribute paths (spec §5.7).
|
||||
*/
|
||||
final class DiviCatalogAbility implements Ability
|
||||
final class DiviCatalogAbility implements Ability, RequiresDiviFeatures
|
||||
{
|
||||
public const CODE_NOTE = 'Code-Modul (code) nur verwenden, wenn kein natives Modul passt – es ist im '
|
||||
. 'Divi-Builder nicht visuell bearbeitbar.';
|
||||
@@ -116,6 +118,12 @@ final class DiviCatalogAbility implements Ability
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Only the module catalog (DiviModules::catalog()). */
|
||||
public function diviFeatures(array $input): array
|
||||
{
|
||||
return [DiviFeature::Catalog];
|
||||
}
|
||||
|
||||
public function permission(array $input): bool|WP_Error
|
||||
{
|
||||
return current_user_can('edit_posts')
|
||||
|
||||
@@ -12,6 +12,8 @@ use LIT\MCP\Abilities\Rollback;
|
||||
use LIT\MCP\Abilities\ToolResult;
|
||||
use LIT\MCP\Abilities\WriteFailed;
|
||||
use LIT\MCP\ChangeLog\ChangeLog;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use LIT\MCP\Integrations\Divi\Release\RequiresDiviFeatures;
|
||||
use Throwable;
|
||||
use WP_Error;
|
||||
|
||||
@@ -27,7 +29,7 @@ defined('ABSPATH') || exit;
|
||||
* published, e.g. in the trash (ruling Q16), never needs the layouts and works despite a Theme Builder draft (with
|
||||
* DRAFT_WARNING, ruling Q20). Overlaps name only templates the user may edit, the others as a count (ruling Q21).
|
||||
*/
|
||||
final class TbActivateAbility implements Ability
|
||||
final class TbActivateAbility implements Ability, RequiresDiviFeatures
|
||||
{
|
||||
public const REASON = 'Die Vorlage konnte nicht umgeschaltet werden.';
|
||||
|
||||
@@ -97,6 +99,12 @@ final class TbActivateAbility implements Ability
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Switches _et_enabled of a listed template (Theme Builder functions); the right is a Divi role. */
|
||||
public function diviFeatures(array $input): array
|
||||
{
|
||||
return [DiviFeature::ThemeBuilderRead, DiviFeature::ThemeBuilderWrite, DiviFeature::Roles];
|
||||
}
|
||||
|
||||
public function permission(array $input): bool|WP_Error
|
||||
{
|
||||
$template = $this->access->changeableTemplate($input, ($input['enabled'] ?? null) === false);
|
||||
|
||||
@@ -12,6 +12,8 @@ use LIT\MCP\Abilities\Rollback;
|
||||
use LIT\MCP\Abilities\ToolResult;
|
||||
use LIT\MCP\Abilities\WriteFailed;
|
||||
use LIT\MCP\ChangeLog\ChangeLog;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use LIT\MCP\Integrations\Divi\Release\RequiresDiviFeatures;
|
||||
use Throwable;
|
||||
use WP_Error;
|
||||
|
||||
@@ -30,7 +32,7 @@ defined('ABSPATH') || exit;
|
||||
* Divi's "unused" marks a reattach removed (review M-12 c): the template is detached again, but Divi no longer
|
||||
* trashes it after seven days – harmless, it stays under detached in lit/tb-list.
|
||||
*/
|
||||
final class TbAssignAbility implements Ability
|
||||
final class TbAssignAbility implements Ability, RequiresDiviFeatures
|
||||
{
|
||||
public const REASON = 'Die Bedingungen der Vorlage konnten nicht gespeichert werden.';
|
||||
|
||||
@@ -103,6 +105,12 @@ final class TbAssignAbility implements Ability
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Reads and writes templates and their conditions (Theme Builder functions); the right is a Divi role. */
|
||||
public function diviFeatures(array $input): array
|
||||
{
|
||||
return [DiviFeature::ThemeBuilderRead, DiviFeature::ThemeBuilderWrite, DiviFeature::Roles];
|
||||
}
|
||||
|
||||
public function permission(array $input): bool|WP_Error
|
||||
{
|
||||
$template = $this->access->changeableTemplate($input);
|
||||
|
||||
@@ -17,6 +17,9 @@ use LIT\MCP\Integrations\Divi\Pages\DiviComposition;
|
||||
use LIT\MCP\Integrations\Divi\Pages\DiviSaver;
|
||||
use LIT\MCP\Integrations\Divi\Pages\LayoutBuilder;
|
||||
use LIT\MCP\Integrations\Divi\Pages\SaveScope;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use LIT\MCP\Integrations\Divi\Release\FeatureSets;
|
||||
use LIT\MCP\Integrations\Divi\Release\RequiresDiviFeatures;
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
use WP_Error;
|
||||
@@ -33,7 +36,7 @@ defined('ABSPATH') || exit;
|
||||
* (TemplateBuild). Refused while the Theme Builder has an unsaved draft (E7). Never enables anything (F1, Q5):
|
||||
* Divi stores it with enabled '0' and a template that reads back enabled or as default is rolled back.
|
||||
*/
|
||||
final class TbCreateTemplateAbility implements Ability
|
||||
final class TbCreateTemplateAbility implements Ability, RequiresDiviFeatures
|
||||
{
|
||||
public const SOURCES = ['new', 'global', 'default', 'hidden'];
|
||||
|
||||
@@ -127,6 +130,29 @@ final class TbCreateTemplateAbility implements Ability
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates layouts and the template (Theme Builder functions); layouts with nodes are composed and saved like a
|
||||
* page (PageRead, PageWrite, Catalog, DesignRead, DynamicSources) – only when an area names nodes.
|
||||
*/
|
||||
public function diviFeatures(array $input): array
|
||||
{
|
||||
$features = [DiviFeature::ThemeBuilderRead, DiviFeature::ThemeBuilderWrite, DiviFeature::Roles];
|
||||
foreach (Template::AREAS as $area) {
|
||||
$spec = $input[$area] ?? null;
|
||||
if (is_array($spec) && is_array($spec['nodes'] ?? null) && $spec['nodes'] !== []) {
|
||||
return FeatureSets::merge($features, [
|
||||
DiviFeature::PageRead,
|
||||
DiviFeature::PageWrite,
|
||||
DiviFeature::Catalog,
|
||||
DiviFeature::DesignRead,
|
||||
DiviFeature::DynamicSources,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return $features;
|
||||
}
|
||||
|
||||
public function permission(array $input): bool|WP_Error
|
||||
{
|
||||
return $this->access->canCreate() ?? $this->access->draftRefusal() ?? true;
|
||||
|
||||
@@ -7,6 +7,8 @@ namespace LIT\MCP\Integrations\Divi\ThemeBuilder;
|
||||
use LIT\MCP\Abilities\Ability;
|
||||
use LIT\MCP\Abilities\Annotations;
|
||||
use LIT\MCP\Abilities\Errors;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use LIT\MCP\Integrations\Divi\Release\RequiresDiviFeatures;
|
||||
use LIT\MCP\Integrations\WordPress\PostGuard;
|
||||
use WP_Error;
|
||||
|
||||
@@ -22,7 +24,7 @@ defined('ABSPATH') || exit;
|
||||
* reads like a missing one – P12) – active ones only as a count (hidden_live, ruling Q21); layout titles only for
|
||||
* layouts the user may edit; condition labels name objects by the stored ID only, never by title.
|
||||
*/
|
||||
final class TbListAbility implements Ability
|
||||
final class TbListAbility implements Ability, RequiresDiviFeatures
|
||||
{
|
||||
public const DETACHED_LIMIT = 50;
|
||||
|
||||
@@ -97,6 +99,12 @@ final class TbListAbility implements Ability
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Templates, layouts and conditions come from Divi's Theme Builder functions; the right is a Divi role. */
|
||||
public function diviFeatures(array $input): array
|
||||
{
|
||||
return [DiviFeature::ThemeBuilderRead, DiviFeature::Roles];
|
||||
}
|
||||
|
||||
public function permission(array $input): bool|WP_Error
|
||||
{
|
||||
return $this->access->canUse() ?? true;
|
||||
|
||||
@@ -35,6 +35,15 @@ interface ContentComposer
|
||||
*/
|
||||
public function refusal(): ?WP_Error;
|
||||
|
||||
/**
|
||||
* IDs of the capabilities of the composing integration that composing a new entry needs (the Divi integration:
|
||||
* the DiviFeature values). lit/content-create hands them on as DeclaresRequirements; the core never interprets
|
||||
* them.
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
public function requirements(): array;
|
||||
|
||||
/**
|
||||
* Validates the description for a new entry of $postType. Nothing is written.
|
||||
*
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace LIT\MCP\Integrations\WordPress\Content;
|
||||
|
||||
use LIT\MCP\Abilities\Ability;
|
||||
use LIT\MCP\Abilities\Annotations;
|
||||
use LIT\MCP\Abilities\DeclaresRequirements;
|
||||
use LIT\MCP\Abilities\Errors;
|
||||
use LIT\MCP\Abilities\Rev;
|
||||
use LIT\MCP\Abilities\ToolResult;
|
||||
@@ -32,7 +33,7 @@ defined('ABSPATH') || exit;
|
||||
* (title/excerpt keep kses), then the composer stores the layout (DiviSaver's kses window, spec
|
||||
* D7) – a refusal or failure there deletes the draft again.
|
||||
*/
|
||||
final class ContentCreateAbility implements Ability
|
||||
final class ContentCreateAbility implements Ability, DeclaresRequirements
|
||||
{
|
||||
/** The extras (written after the layout) re-saved the entry and changed the stored layout (review m-6). */
|
||||
public const LAYOUT_CHANGED = 'Der Divi-Inhalt wurde beim Speichern der Felder verändert (z. B. durch ein Plugin); '
|
||||
@@ -109,6 +110,15 @@ final class ContentCreateAbility implements Ability
|
||||
return $type instanceof WP_Error ? $type : true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Without divi the call stays in WordPress and needs nothing; with divi it needs what the ContentComposer says it
|
||||
* needs (the Divi integration names its capabilities), so the guard refuses before a draft is inserted.
|
||||
*/
|
||||
public function requirements(array $input): array
|
||||
{
|
||||
return array_key_exists('divi', $input) && $this->divi !== null ? $this->divi->requirements() : [];
|
||||
}
|
||||
|
||||
public function execute(array $input): mixed
|
||||
{
|
||||
$type = PostGuard::postType($input['post_type'] ?? null, $this->schema, 'create_posts');
|
||||
|
||||
Reference in New Issue
Block a user