feat: Divi read tools accept library entries (library access, E1/E3)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,8 +6,12 @@ namespace LIT\MCP\Integrations\Divi;
|
||||
|
||||
use Closure;
|
||||
use LIT\MCP\ChangeLog\ChangeLog;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\Parser;
|
||||
use LIT\MCP\Integrations\Divi\Design\DesignSystemReader;
|
||||
use LIT\MCP\Integrations\Divi\Design\DiviDesignSystemAbility;
|
||||
use LIT\MCP\Integrations\Divi\Library\DiviLibraryStore;
|
||||
use LIT\MCP\Integrations\Divi\Library\DiviRoles;
|
||||
use LIT\MCP\Integrations\Divi\Library\LibraryAccess;
|
||||
use LIT\MCP\Integrations\Divi\Pages\DiviComposition;
|
||||
use LIT\MCP\Integrations\Divi\Pages\DiviContentComposer;
|
||||
use LIT\MCP\Integrations\Divi\Pages\DiviDeleteAbility;
|
||||
@@ -39,11 +43,23 @@ final class DiviIntegration implements Integration
|
||||
{
|
||||
private DiviModules $modules;
|
||||
private DesignSystemReader $design;
|
||||
private LibraryAccess $access;
|
||||
|
||||
public function __construct(private ContentSchema $schema, private ChangeLog $log)
|
||||
{
|
||||
/**
|
||||
* @param (Closure(): bool)|null $libraryWritable Tools-tab gate for writes to library entries
|
||||
* (group "Divi-Bibliothek" writable); null = always.
|
||||
* @param (Closure(): bool)|null $libraryVisible Tools-tab gate for library entries at all (group
|
||||
* "Divi-Bibliothek" not disabled, ruling P9); null = always.
|
||||
*/
|
||||
public function __construct(
|
||||
private ContentSchema $schema,
|
||||
private ChangeLog $log,
|
||||
?Closure $libraryWritable = null,
|
||||
?Closure $libraryVisible = null
|
||||
) {
|
||||
$this->modules = new DiviModules();
|
||||
$this->design = new DesignSystemReader();
|
||||
$this->access = new LibraryAccess(new DiviLibraryStore(), new DiviRoles(), $libraryWritable, $libraryVisible);
|
||||
}
|
||||
|
||||
public function id(): string
|
||||
@@ -80,7 +96,7 @@ final class DiviIntegration implements Integration
|
||||
|
||||
public function abilities(): array
|
||||
{
|
||||
$loader = new DiviPageLoader($this->schema);
|
||||
$loader = new DiviPageLoader($this->schema, new Parser(), $this->access);
|
||||
$composition = new DiviComposition($this->modules, $this->design);
|
||||
$writer = new DiviWriter($loader, new DiviSaver(), $this->log, $this->design, $composition);
|
||||
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Library;
|
||||
|
||||
use Closure;
|
||||
use LIT\MCP\Abilities\Errors;
|
||||
use LIT\MCP\Integrations\WordPress\PostGuard;
|
||||
use WP_Error;
|
||||
use WP_Post;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* Rights on Divi Library entries (spec §4.5, E3 – never laxer than Divi): the library at all needs
|
||||
* edit_posts and the Divi role option divi_library; one entry additionally edit_post on it (Divi's
|
||||
* builder routes do not check that, F9). The Divi tools reach library entries only through
|
||||
* item(); every other ID goes on to PostGuard unchanged.
|
||||
* Not final so abilities can be tested with a mock.
|
||||
*/
|
||||
class LibraryAccess
|
||||
{
|
||||
/** Tool group "Divi-Bibliothek" (Tools tab); its switches gate library entries (E9, ruling P9). */
|
||||
public const GROUP = 'divi-library';
|
||||
|
||||
/**
|
||||
* @param (Closure(): bool)|null $writable Whether the Tools tab lets this connection write to the
|
||||
* library (group "Divi-Bibliothek" not read-only or disabled); null = always.
|
||||
* @param (Closure(): bool)|null $visible Whether library entries exist for this connection at all
|
||||
* (group "Divi-Bibliothek" not disabled, ruling P9); null = always.
|
||||
*/
|
||||
public function __construct(
|
||||
private DiviLibraryStore $store,
|
||||
private DiviRoles $roles,
|
||||
private ?Closure $writable = null,
|
||||
private ?Closure $visible = null
|
||||
) {
|
||||
}
|
||||
|
||||
public function store(): DiviLibraryStore
|
||||
{
|
||||
return $this->store;
|
||||
}
|
||||
|
||||
public function roles(): DiviRoles
|
||||
{
|
||||
return $this->roles;
|
||||
}
|
||||
|
||||
/**
|
||||
* The library entry $postId after the checks for reading, or their refusal; null when $postId
|
||||
* names no library entry (the caller goes on with PostGuard). With the library hidden (ruling
|
||||
* P9), an entry is refused exactly like an ID that does not exist – before any rights check.
|
||||
*/
|
||||
public function item(mixed $postId, bool $forWrite): WP_Post|WP_Error|null
|
||||
{
|
||||
$id = PostGuard::id($postId);
|
||||
$post = $id === null ? null : get_post($id);
|
||||
if (! $post instanceof WP_Post || $post->post_type !== DiviLibraryStore::POST_TYPE) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->visible !== null && ! ($this->visible)()) {
|
||||
return PostGuard::notFound((int) $post->ID);
|
||||
}
|
||||
|
||||
return $this->canRead($post) ?? $post;
|
||||
}
|
||||
|
||||
/** Listing the library: edit_posts and the Divi role option divi_library. */
|
||||
public function canList(): ?WP_Error
|
||||
{
|
||||
if (! current_user_can('edit_posts')) {
|
||||
return Errors::forbidden('Keine Berechtigung, die Divi-Bibliothek zu nutzen.');
|
||||
}
|
||||
|
||||
return $this->roles->refusal('divi_library');
|
||||
}
|
||||
|
||||
/** Reading one entry: canList() and edit_post on the entry. */
|
||||
public function canRead(WP_Post $item): ?WP_Error
|
||||
{
|
||||
$refusal = $this->canList();
|
||||
if ($refusal !== null) {
|
||||
return $refusal;
|
||||
}
|
||||
|
||||
return current_user_can('edit_post', (int) $item->ID)
|
||||
? null
|
||||
: Errors::forbidden(sprintf('Keine Berechtigung, Bibliothekseintrag %d zu bearbeiten.', (int) $item->ID));
|
||||
}
|
||||
|
||||
public function isGlobal(WP_Post $item): bool
|
||||
{
|
||||
return $this->store->isGlobal((int) $item->ID);
|
||||
}
|
||||
}
|
||||
@@ -21,10 +21,17 @@ final class DiviPage
|
||||
public function __construct(
|
||||
public readonly WP_Post $post,
|
||||
public readonly Document $document,
|
||||
public readonly NodeIndex $index
|
||||
public readonly NodeIndex $index,
|
||||
private bool $libraryItem = false
|
||||
) {
|
||||
}
|
||||
|
||||
/** Whether the entry is a Divi Library entry (et_pb_layout), loaded through LibraryAccess. */
|
||||
public function isLibraryItem(): bool
|
||||
{
|
||||
return $this->libraryItem;
|
||||
}
|
||||
|
||||
public function treeRev(): string
|
||||
{
|
||||
return Rev::of((string) $this->post->post_content);
|
||||
@@ -40,7 +47,10 @@ final class DiviPage
|
||||
|
||||
public function editUrl(): string
|
||||
{
|
||||
return self::builderUrl($this->post);
|
||||
// A library entry has no public permalink (public: false); Divi's library list opens it here.
|
||||
return $this->libraryItem
|
||||
? (string) get_edit_post_link($this->post, 'raw')
|
||||
: self::builderUrl($this->post);
|
||||
}
|
||||
|
||||
/** The entry opened in the Divi Visual Builder – the edit_url of every Divi write (review I-1). */
|
||||
|
||||
@@ -8,31 +8,45 @@ use LIT\MCP\Abilities\Errors;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\MalformedMarkup;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\NodeIndex;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\Parser;
|
||||
use LIT\MCP\Integrations\Divi\Library\LibraryAccess;
|
||||
use LIT\MCP\Integrations\WordPress\PostGuard;
|
||||
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
|
||||
use LIT\MCP\Log;
|
||||
use WP_Error;
|
||||
use WP_Post;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* Loads an entry for the Divi tools: rights and status via PostGuard (spec §4.5), then the
|
||||
* block tree. Divi 4 shortcode content and unbalanced markup are refused with a hint –
|
||||
* spec §3.4: Divi 4 content is kept, never edited.
|
||||
* Loads an entry for the Divi tools: Divi Library entries via LibraryAccess (E1/E3), every other
|
||||
* entry with its rights and status via PostGuard (spec §4.5), then the block tree. Divi 4
|
||||
* shortcode content and unbalanced markup are refused with a hint – spec §3.4: Divi 4 content
|
||||
* is kept, never edited.
|
||||
*/
|
||||
final class DiviPageLoader
|
||||
{
|
||||
public const OUTLINE_HINT = 'Aktuellen Stand mit lit/divi-outline (direkt: lit-divi-outline) neu lesen und die '
|
||||
. 'Änderung mit der neuen tree_rev wiederholen.';
|
||||
|
||||
public function __construct(private ContentSchema $schema, private Parser $parser = new Parser())
|
||||
public function __construct(
|
||||
private ContentSchema $schema,
|
||||
private Parser $parser = new Parser(),
|
||||
private ?LibraryAccess $library = null
|
||||
) {
|
||||
}
|
||||
|
||||
/** The library access the Divi tools use, null when library entries are not reachable. */
|
||||
public function library(): ?LibraryAccess
|
||||
{
|
||||
return $this->library;
|
||||
}
|
||||
|
||||
/** Readers pass $forWrite = false (trashed entries may be read, like lit-content-get). */
|
||||
public function load(mixed $postId, bool $forWrite): DiviPage|WP_Error
|
||||
{
|
||||
$post = PostGuard::editable($postId, $this->schema, 'post_id', ! $forWrite);
|
||||
// Library entries (E1): only for reading so far, and only with a library access.
|
||||
$item = $forWrite ? null : $this->library?->item($postId, false);
|
||||
$post = $item ?? PostGuard::editable($postId, $this->schema, 'post_id', ! $forWrite);
|
||||
if ($post instanceof WP_Error) {
|
||||
return $post;
|
||||
}
|
||||
@@ -64,7 +78,7 @@ final class DiviPageLoader
|
||||
);
|
||||
}
|
||||
|
||||
return new DiviPage($post, $document, NodeIndex::build($document));
|
||||
return new DiviPage($post, $document, NodeIndex::build($document), $item instanceof WP_Post);
|
||||
}
|
||||
|
||||
/** The entry loaded for writing and checked by writable() – the refusal of either step. */
|
||||
|
||||
Reference in New Issue
Block a user