Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
180 lines
6.6 KiB
PHP
180 lines
6.6 KiB
PHP
<?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');
|
||
}
|
||
|
||
/** 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);
|
||
}
|
||
}
|