Files
WP-LIT-MCP/src/Integrations/Divi/Library/LibraryAccess.php
T
janniclas.loosenandClaude Opus 5.5 c7ce924e5a fix: library IDs stay hidden without library access (ruling P13)
Outline, node-get, write outlines, the render summary and replace-text's
opaque-node match name no library ID unless the library group is visible
and the user has divi_library; read-only still names them.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-27 15:00:39 +02:00

191 lines
7.1 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\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) or without the library right at all (ruling P12), an entry is refused exactly like an ID
* that does not exist, so its type and existence stay unknown; only a user with the library
* right who lacks edit_post on the entry gets forbidden (like pages). $forWrite adds canEdit().
*/
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)()) || $this->canList() !== null) {
return PostGuard::notFound((int) $post->ID);
}
return ($forWrite ? $this->canEdit($post) : $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');
}
/**
* Ruling P13: whether library IDs may be named to this connection at all – the library group
* is not disabled (P9; "nur lesen" still names them) and the user has the library right
* (canList). Otherwise outlines, node-get, write results, text search and the render summary
* say "global module" without the entry's ID, as if it were not stored.
*/
public function revealsIds(): bool
{
return ($this->visible === null || ($this->visible)()) && $this->canList() === null;
}
/** 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));
}
/**
* Changing one entry: canRead(), the role option save_library, the Tools-tab gate and a live
* entry; a global entry additionally needs edit_global_library (E3 – Divi's builder route
* does not check edit_post on the entry, F9; LIT MCP does).
*/
public function canEdit(WP_Post $item): ?WP_Error
{
$refusal = $this->canRead($item) ?? $this->roles->refusal('save_library') ?? $this->writableGate();
if ($refusal !== null) {
return $refusal;
}
if ((string) $item->post_status !== 'publish') {
return Errors::invalidInput(
sprintf(
'Bibliothekseintrag %d ist nicht aktiv (Status „%s“); nichts wurde gespeichert.',
(int) $item->ID,
(string) $item->post_status
),
['post_id' => 'nur aktive Bibliothekseinträge'],
'Den Eintrag zuerst in der Divi-Bibliothek wiederherstellen.'
);
}
return $this->isGlobal($item) ? $this->roles->refusal('edit_global_library') : null;
}
/**
* Creating an entry: canList(), publish_posts of et_pb_layout (library entries are always
* published, F2), the role option save_library and the Tools-tab gate; a global entry
* additionally edit_global_library (ruling P7 – the same option as for editing one, F12).
*/
public function canSave(bool $global = false): ?WP_Error
{
$refusal = $this->canList();
if ($refusal !== null) {
return $refusal;
}
$type = get_post_type_object(DiviLibraryStore::POST_TYPE);
$publish = is_object($type) && isset($type->cap->publish_posts)
? (string) $type->cap->publish_posts
: 'publish_posts';
if (! current_user_can($publish)) {
return Errors::forbidden('Keine Berechtigung, Einträge der Divi-Bibliothek anzulegen (publish_posts).');
}
$refusal = $this->roles->refusal('save_library') ?? $this->writableGate();
if ($refusal !== null || ! $global) {
return $refusal;
}
return $this->roles->refusal('edit_global_library');
}
/**
* Inserting from the library into a page (E3): canList(), the role option add_library (Divi's
* "Aus der Bibliothek hinzufügen") and a group "Divi-Bibliothek" that is not read-only or
* disabled (E9). Reading the entry itself stays item() (ruling P5).
*/
public function canInsert(): ?WP_Error
{
return $this->canList() ?? $this->roles->refusal('add_library') ?? $this->writableGate();
}
/** Refusal while the Tools tab keeps this connection from writing to the library. */
public function writableGate(): ?WP_Error
{
if ($this->writable === null || ($this->writable)()) {
return null;
}
return Errors::forbidden(
'Schreiben in die Divi-Bibliothek ist für diese Verbindung abgeschaltet (Gruppe „Divi-Bibliothek“ auf '
. '„nur lesen“ oder deaktiviert).',
'Einen Administrator fragen: Einstellungen → LIT MCP → Tools.'
);
}
public function isGlobal(WP_Post $item): bool
{
return $this->store->isGlobal((int) $item->ID);
}
}