Files
WP-LIT-MCP/src/Integrations/Divi/Pages/DiviNodeCopyAbility.php
T

174 lines
7.0 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\Pages;
use Closure;
use LIT\MCP\Abilities\Annotations;
use LIT\MCP\Abilities\Errors;
use WP_Error;
defined('ABSPATH') || exit;
/**
* lit/divi-node-copy (spec §5.7, E6): a node with everything inside from one Divi entry (page or
* library entry) to a position in another. The copy keeps its bytes – global instances stay linked –
* and passes StoredNodeGuard of the target (builder-only values never move to another post for users
* without unfiltered_html). Source: edit rights via DiviPageLoader (PostGuard / LibraryAccess, P9
* hiding) and its own source_tree_rev, because path IDs are only valid for the state they were read
* in; the source is only read, so it needs no lock. Target: like lit-divi-insert (tree_rev lock,
* StructureRules, all or nothing, DiviWriter::commit with GlobalEdits – a global target entry never
* gets a nested instance). Follows lit/divi-insert's Tools-tab state (ruling P9/E9). A library entry
* as source is an insert from the library (E3, audit L5): it needs LibraryAccess::canInsert()
* (add_library, writable group "Divi-Bibliothek") and follows lit-library-insert's Tools-tab state.
*/
final class DiviNodeCopyAbility extends DiviWriteAbility
{
/**
* @param (Closure(): bool)|null $diviWritable Whether lit/divi-insert is enabled; null = always.
* @param (Closure(): bool)|null $libraryInsertable Whether lit/library-insert is enabled (gates
* library entries as source); null = always.
*/
public function __construct(
DiviWriter $writer,
private DiviPageLoader $loader,
private ?Closure $diviWritable = null,
private ?Closure $libraryInsertable = null
) {
parent::__construct($writer);
}
public function name(): string
{
return 'lit/divi-node-copy';
}
public function label(): string
{
return 'Divi-Knoten kopieren';
}
public function description(): string
{
return 'Kopiert einen Divi-Knoten samt Inhalt von einem Eintrag (source_post_id, source_id und '
. 'source_tree_rev aus lit-divi-outline der Quelle) an eine Position eines anderen Eintrags (post_id, '
. 'tree_rev, parent/before/after). Die Kopie ist byte-gleich; globale Module bleiben verknüpft. Quelle '
. 'und Ziel dürfen auch Bibliothekseinträge sein. Alles oder nichts.';
}
public function inputSchema(): array
{
return [
'type' => 'object',
'properties' => array_merge(DiviSchema::write(), [
'source_post_id' => ['type' => 'integer', 'minimum' => 1],
'source_id' => ['type' => 'string', 'description' => 'Pfad-ID des Knotens in der Quelle.'],
'source_tree_rev' => ['type' => 'string', 'description' => 'tree_rev der Quelle (lit-divi-outline).'],
], DiviSchema::position()),
'required' => ['post_id', 'tree_rev', 'source_post_id', 'source_id', 'source_tree_rev'],
'additionalProperties' => false,
];
}
public function annotations(): array
{
return Annotations::write(false);
}
public function isDirect(): bool
{
return false;
}
/**
* lit/divi-insert's Tools-tab state, then the source's own rights and, for a library entry as
* source, the rights of an insert from the library – in permission() and execute().
*/
protected function precheck(array $input): ?WP_Error
{
if ($this->diviWritable !== null && ! ($this->diviWritable)()) {
return Errors::unsupported(
'Divi-Änderungen sind für diese Verbindung abgeschaltet (Gruppe „Divi“ auf „nur lesen“ oder '
. 'lit-divi-insert deaktiviert).',
'Einen Administrator fragen: Einstellungen → LIT MCP → Tools.'
);
}
$source = $this->loader->load($input['source_post_id'] ?? null, false);
if ($source instanceof WP_Error) {
return $source;
}
return $source->isLibraryItem() ? $this->libraryInsertRefusal() : null;
}
/** E3/audit L5: copying out of a library entry is inserting from the library (lit-library-insert). */
private function libraryInsertRefusal(): ?WP_Error
{
if ($this->libraryInsertable !== null && ! ($this->libraryInsertable)()) {
return Errors::unsupported(
'Einfügen aus der Divi-Bibliothek ist für diese Verbindung abgeschaltet (lit-library-insert '
. 'deaktiviert oder Gruppe „Divi-Bibliothek“ auf „nur lesen“).',
'Einen Administrator fragen: Einstellungen → LIT MCP → Tools.'
);
}
return $this->loader->library()?->canInsert();
}
protected function change(DiviPage $page, array $input, DiviComposition $composition): DiviChange|WP_Error
{
$source = $this->loader->load($input['source_post_id'] ?? null, false);
if ($source instanceof WP_Error) {
return $source;
}
if (! is_string($input['source_tree_rev'] ?? null) || $input['source_tree_rev'] !== $source->treeRev()) {
return Errors::revMismatch(
'Die Quelle wurde seit dem letzten Lesen geändert (source_tree_rev passt nicht); nichts wurde '
. 'gespeichert.',
'Die Quelle mit lit-divi-outline neu lesen und source_tree_rev sowie die Pfad-ID übernehmen.'
);
}
$id = is_string($input['source_id'] ?? null) ? $input['source_id'] : '';
$node = $source->index->find($id);
if ($node === null) {
return DiviErrors::unknownNode($id, $source->post);
}
$copy = $node->copy();
if ($source->isLibraryItem()) {
// Finding 9: library entries from Divi's dialog use CRLF; unified before every takeover
// (LibraryContent::load), as lit-library-insert and lit/library-save do.
$copy->unifyLineBreaks();
}
$problems = $composition->storedGuard()->problems($copy);
if ($problems !== []) {
return Errors::invalidInput(
'Der Knoten enthält Einstellungen, die nur im Divi-Builder übertragen werden dürfen; nichts wurde '
. 'gespeichert.',
['source_id' => implode('; ', $problems)]
);
}
$editor = $composition->editor();
$position = $editor->position($page->index, $input);
if (is_string($position)) {
return DiviErrors::position($position);
}
$violation = $editor->insert($position, [$copy]);
if ($violation !== null) {
return Errors::invalidInput(
'Der Knoten passt nicht an diese Stelle; nichts wurde gespeichert.',
['source_id' => $violation]
);
}
return (new DiviChange(sprintf('Knoten %s aus Eintrag %d kopiert', $id, (int) $source->post->ID)))
->record($copy, 'inserted');
}
}