feat: group Divi-Bibliothek with lit/library-list
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Library;
|
||||
|
||||
use LIT\MCP\Integrations\Divi\Design\DesignSystemReader;
|
||||
use LIT\MCP\Integrations\Divi\Pages\DiviPageLoader;
|
||||
use LIT\MCP\Integrations\Divi\Schema\DiviModules;
|
||||
use LIT\MCP\Integrations\Integration;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* The group "Divi-Bibliothek" (E9): lit/library-list, later lit/library-save and lit-library-insert.
|
||||
* Built by DiviIntegration::library() so it shares the library access (rights, Tools-tab gates –
|
||||
* its id LibraryAccess::GROUP is what the P9 hiding and the write gate ask). Available with the
|
||||
* Divi 5 builder, like the Divi group.
|
||||
*/
|
||||
final class LibraryIntegration implements Integration
|
||||
{
|
||||
public function __construct(
|
||||
private LibraryAccess $access,
|
||||
private DiviPageLoader $loader,
|
||||
private DesignSystemReader $design
|
||||
) {
|
||||
}
|
||||
|
||||
public function id(): string
|
||||
{
|
||||
return LibraryAccess::GROUP;
|
||||
}
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return __('Divi-Bibliothek', 'wp-lit-mcp');
|
||||
}
|
||||
|
||||
public function isAvailable(): bool
|
||||
{
|
||||
return DiviModules::isAvailable();
|
||||
}
|
||||
|
||||
public function abilities(): array
|
||||
{
|
||||
return [
|
||||
new LibraryListAbility($this->access, $this->loader, $this->design, new GlobalUsages()),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Library;
|
||||
|
||||
use LIT\MCP\Abilities\Ability;
|
||||
use LIT\MCP\Abilities\Annotations;
|
||||
use LIT\MCP\Abilities\Cursor;
|
||||
use LIT\MCP\Abilities\Errors;
|
||||
use LIT\MCP\Abilities\Rev;
|
||||
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\WordPress\PostGuard;
|
||||
use WP_Error;
|
||||
use WP_Post;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* lit/library-list: the Divi Library (spec §5.7, E9 – replaces lit/library-get). Without id a page
|
||||
* of published entries (filters, cursor); with id one entry with its node outline, tree_rev and,
|
||||
* for a global entry, the pages that show it. Rights: LibraryAccess (edit_posts + divi_library;
|
||||
* every listed entry also edit_post – others are 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
|
||||
{
|
||||
/** At most this many entries are looked at per call while skipping entries the user may not edit. */
|
||||
public const MAX_SCAN = 500;
|
||||
|
||||
public function __construct(
|
||||
private LibraryAccess $access,
|
||||
private DiviPageLoader $loader,
|
||||
private DesignSystemReader $design,
|
||||
private GlobalUsages $usages
|
||||
) {
|
||||
}
|
||||
|
||||
public function name(): string
|
||||
{
|
||||
return 'lit/library-list';
|
||||
}
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return 'Divi-Bibliothek auflisten';
|
||||
}
|
||||
|
||||
public function description(): string
|
||||
{
|
||||
return 'Listet Einträge der Divi-Bibliothek (Layouts, Abschnitte, Zeilen, Module; normal oder global), '
|
||||
. 'filterbar nach layout_type, scope, search und category, blätterbar mit cursor. Mit id: ein Eintrag '
|
||||
. 'im Detail – Knoten-Übersicht, tree_rev und bei globalen Einträgen die Seiten, die ihn zeigen '
|
||||
. '(affected). Einfügen mit lit-library-insert; bearbeiten mit den Divi-Werkzeugen und der ID als post_id.';
|
||||
}
|
||||
|
||||
public function inputSchema(): array
|
||||
{
|
||||
return [
|
||||
'type' => 'object',
|
||||
'properties' => array_merge([
|
||||
'id' => ['type' => 'integer', 'minimum' => 1, 'description' => 'Ein Eintrag im Detail.'],
|
||||
'layout_type' => ['type' => 'string', 'enum' => DiviLibraryStore::LAYOUT_TYPES],
|
||||
'scope' => ['type' => 'string', 'enum' => ['normal', 'global']],
|
||||
'search' => ['type' => 'string', 'maxLength' => 100, 'description' => 'Suche im Titel.'],
|
||||
'category' => ['type' => 'string', 'maxLength' => 100, 'description' => 'Name einer Kategorie.'],
|
||||
], Cursor::inputProperties()),
|
||||
'additionalProperties' => false,
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(): ?array
|
||||
{
|
||||
return [
|
||||
'type' => 'object',
|
||||
'properties' => [
|
||||
'items' => [
|
||||
'type' => 'array',
|
||||
'items' => [
|
||||
'type' => 'object',
|
||||
'properties' => [
|
||||
'id' => ['type' => 'integer'],
|
||||
'title' => ['type' => 'string'],
|
||||
'layout_type' => ['type' => ['string', 'null']],
|
||||
'scope' => ['type' => 'string'],
|
||||
'module_type' => ['type' => ['string', 'null']],
|
||||
'categories' => ['type' => 'array', 'items' => ['type' => 'string']],
|
||||
'tags' => ['type' => 'array', 'items' => ['type' => 'string']],
|
||||
'modified' => ['type' => 'string'],
|
||||
],
|
||||
'required' => [
|
||||
'id', 'title', 'layout_type', 'scope', 'module_type', 'categories', 'tags', 'modified',
|
||||
],
|
||||
],
|
||||
],
|
||||
'next_cursor' => ['type' => ['string', 'null']],
|
||||
'outline' => ['type' => 'string'],
|
||||
'tree_rev' => ['type' => 'string'],
|
||||
'rev' => ['type' => 'string'],
|
||||
'edit_url' => ['type' => 'string'],
|
||||
'affected' => ['type' => 'array', 'items' => ['type' => 'object']],
|
||||
'affected_count' => ['type' => 'integer'],
|
||||
],
|
||||
'required' => ['items', 'next_cursor'],
|
||||
];
|
||||
}
|
||||
|
||||
public function annotations(): array
|
||||
{
|
||||
return Annotations::read();
|
||||
}
|
||||
|
||||
public function isDirect(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function permission(array $input): bool|WP_Error
|
||||
{
|
||||
if (isset($input['id'])) {
|
||||
$page = $this->entry($input['id']);
|
||||
|
||||
return $page instanceof WP_Error ? $page : true;
|
||||
}
|
||||
|
||||
return $this->access->canList() ?? true;
|
||||
}
|
||||
|
||||
public function execute(array $input): mixed
|
||||
{
|
||||
if (isset($input['id'])) {
|
||||
$page = $this->entry($input['id']);
|
||||
|
||||
return $page instanceof WP_Error ? $page : $this->detail($page);
|
||||
}
|
||||
|
||||
$refusal = $this->access->canList();
|
||||
if ($refusal !== null) {
|
||||
return $refusal;
|
||||
}
|
||||
|
||||
$offset = Cursor::decode(is_string($input['cursor'] ?? null) ? $input['cursor'] : null);
|
||||
if ($offset === null) {
|
||||
return Cursor::invalid();
|
||||
}
|
||||
|
||||
$filters = array_filter(
|
||||
[
|
||||
'layout_type' => $input['layout_type'] ?? null,
|
||||
'scope' => $input['scope'] ?? null,
|
||||
'search' => $input['search'] ?? null,
|
||||
'category' => $input['category'] ?? null,
|
||||
],
|
||||
static fn (mixed $value): bool => is_string($value) && $value !== ''
|
||||
);
|
||||
|
||||
return $this->page($filters, Cursor::limit($input['limit'] ?? null), $offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* One page of the entries the user may edit. The cursor counts the store's rows, so entries
|
||||
* left out never shift a page; next_cursor points at the next readable entry, null when none
|
||||
* follows (or the scan cap is reached with nothing left).
|
||||
*
|
||||
* @param array<string, string> $filters
|
||||
* @return array{items: list<array<string, mixed>>, next_cursor: ?string}
|
||||
*/
|
||||
private function page(array $filters, int $limit, int $offset): array
|
||||
{
|
||||
$store = $this->access->store();
|
||||
$items = [];
|
||||
$position = $offset;
|
||||
while ($position - $offset < self::MAX_SCAN) {
|
||||
$rows = $store->find($filters, $limit, $position);
|
||||
foreach ($rows as $post) {
|
||||
if ($this->access->canRead($post) !== null) {
|
||||
++$position;
|
||||
continue;
|
||||
}
|
||||
if (count($items) === $limit) {
|
||||
return ['items' => $items, 'next_cursor' => Cursor::encode($position)];
|
||||
}
|
||||
$items[] = $store->describe($post);
|
||||
++$position;
|
||||
}
|
||||
if (count($rows) <= $limit) {
|
||||
return ['items' => $items, 'next_cursor' => null];
|
||||
}
|
||||
}
|
||||
|
||||
return ['items' => $items, 'next_cursor' => Cursor::encode($position)];
|
||||
}
|
||||
|
||||
/**
|
||||
* The library entry $id loaded for reading. Anything that is not a library entry reads like a
|
||||
* missing ID, so the tool tells nothing about other posts (ruling P12).
|
||||
*/
|
||||
private function entry(mixed $id): DiviPage|WP_Error
|
||||
{
|
||||
$postId = PostGuard::id($id);
|
||||
if ($postId === null) {
|
||||
return Errors::invalidInput('id ist keine gültige Eintrags-ID.', ['id' => 'positive Ganzzahl']);
|
||||
}
|
||||
|
||||
$item = $this->access->item($postId, false);
|
||||
if (! $item instanceof WP_Post) {
|
||||
return $item ?? PostGuard::notFound($postId);
|
||||
}
|
||||
|
||||
$page = $this->loader->load($postId, false);
|
||||
if ($page instanceof WP_Error) {
|
||||
return $page;
|
||||
}
|
||||
|
||||
return $page->isLibraryItem() ? $page : PostGuard::notFound($postId);
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
private function detail(DiviPage $page): array
|
||||
{
|
||||
$item = $this->access->store()->describe($page->post);
|
||||
$outline = new Outline($this->design->read()->presetNames());
|
||||
$result = [
|
||||
'items' => [$item],
|
||||
'next_cursor' => null,
|
||||
'outline' => $outline->text($outline->rows($page->index)),
|
||||
'tree_rev' => $page->treeRev(),
|
||||
'rev' => Rev::ofPost($page->post),
|
||||
'edit_url' => $page->editUrl(),
|
||||
];
|
||||
if ($item['scope'] === 'global') {
|
||||
$usages = $this->usages->of((int) $page->post->ID);
|
||||
$result['affected'] = $usages['items'];
|
||||
$result['affected_count'] = $usages['count'];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user