fix: lit/divi-node-copy from a library entry needs the insert rights (I-1, L5)
A library entry as source now requires LibraryAccess::canInsert() (add_library, writable library group) and follows lit-library-insert's Tools-tab state (disabled -> unsupported). DiviPageLoader::library() gets its caller. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -29,7 +29,9 @@ Die Divi-Bibliothek speichert Layouts, Abschnitte, Zeilen und Module zur Wiederv
|
||||
- `lit/divi-node-copy`: einen Knoten von einem Eintrag an eine Position eines anderen kopieren
|
||||
(`source_post_id`, `source_id`, `source_tree_rev` aus `lit-divi-outline` der Quelle – Pflicht, weil
|
||||
Pfad-IDs nur für diesen Stand gelten; Ziel mit `post_id`, `tree_rev`, Position). Quelle und Ziel dürfen
|
||||
Seiten oder Bibliothekseinträge sein. Die Kopie ist byte-gleich; globale Module bleiben verknüpft.
|
||||
Seiten oder Bibliothekseinträge sein. Die Kopie ist byte-gleich; globale Module bleiben verknüpft. Aus
|
||||
einem Bibliothekseintrag zu kopieren ist Einfügen aus der Bibliothek: es braucht dieselben Rechte und
|
||||
Freigaben wie `lit-library-insert`.
|
||||
- Die Divi-Werkzeuge arbeiten auch auf Bibliothekseinträgen: `lit-divi-outline`, `lit-divi-node-get`,
|
||||
`lit-divi-insert`, `lit-divi-update`, `lit-divi-move`, `lit-divi-delete`, `lit/divi-duplicate-node` und
|
||||
`lit/divi-replace-text` mit der ID des Eintrags als `post_id`.
|
||||
@@ -79,7 +81,8 @@ sie in `notes`; ihren Inhalt mit `lit-divi-outline` und der ID lesen.
|
||||
Hinweis.
|
||||
- Einstellungen → LIT MCP → Tools: Gruppe „Divi-Bibliothek“ auf „nur lesen“ sperrt jeden Schreibzugriff auf
|
||||
Bibliothekseinträge, auch über die Divi-Werkzeuge; deaktiviert gilt jeder Eintrag als nicht vorhanden.
|
||||
`lit-library-insert` und `lit/divi-node-copy` sind außerdem gesperrt, wenn `lit-divi-insert` gesperrt ist.
|
||||
`lit-library-insert` und `lit/divi-node-copy` sind außerdem gesperrt, wenn `lit-divi-insert` gesperrt ist;
|
||||
`lit/divi-node-copy` mit einem Bibliothekseintrag als Quelle auch, wenn `lit-library-insert` gesperrt ist.
|
||||
- Ohne das Recht `unfiltered_html` überträgt LIT MCP beim Kopieren, Einfügen und Speichern (`lit/divi-node-copy`,
|
||||
`lit-library-insert`, `lit/library-save`) nur Knoten, die das Konto auch selbst schreiben dürfte. Abgelehnt
|
||||
werden – „nur im Divi-Builder“ – Knoten mit eigenem HTML, CSS oder HTML-Attributen, Interaktionen, einer
|
||||
|
||||
@@ -58,13 +58,16 @@ final class DiviIntegration implements Integration
|
||||
* "Divi-Bibliothek" not disabled, ruling P9); null = always.
|
||||
* @param (Closure(): bool)|null $diviWritable Whether lit/divi-insert is enabled – lit/divi-node-copy
|
||||
* follows it (ruling P9, E9); null = always.
|
||||
* @param (Closure(): bool)|null $libraryInsertable Whether lit/library-insert is enabled –
|
||||
* lit/divi-node-copy follows it for library entries as source (audit L5); null = always.
|
||||
*/
|
||||
public function __construct(
|
||||
private ContentSchema $schema,
|
||||
private ChangeLog $log,
|
||||
?Closure $libraryWritable = null,
|
||||
?Closure $libraryVisible = null,
|
||||
private ?Closure $diviWritable = null
|
||||
private ?Closure $diviWritable = null,
|
||||
private ?Closure $libraryInsertable = null
|
||||
) {
|
||||
$this->modules = new DiviModules();
|
||||
$this->design = new DesignSystemReader();
|
||||
@@ -119,7 +122,7 @@ final class DiviIntegration implements Integration
|
||||
new DiviDeleteAbility($writer),
|
||||
new DiviDuplicateNodeAbility($writer),
|
||||
new DiviReplaceTextAbility($this->schema, $loader, $writer),
|
||||
new DiviNodeCopyAbility($writer, $loader, $this->diviWritable),
|
||||
new DiviNodeCopyAbility($writer, $loader, $this->diviWritable, $this->libraryInsertable),
|
||||
new DiviDynamicSourcesAbility($this->schema, new DynamicContentMap(), $this->access->roles()),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -19,15 +19,22 @@ defined('ABSPATH') || exit;
|
||||
* 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).
|
||||
* 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 $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 $diviWritable = null,
|
||||
private ?Closure $libraryInsertable = null
|
||||
) {
|
||||
parent::__construct($writer);
|
||||
}
|
||||
@@ -74,7 +81,10 @@ final class DiviNodeCopyAbility extends DiviWriteAbility
|
||||
return false;
|
||||
}
|
||||
|
||||
/** lit/divi-insert's Tools-tab state, then the source's own rights – in permission() and execute(). */
|
||||
/**
|
||||
* 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)()) {
|
||||
@@ -86,8 +96,25 @@ final class DiviNodeCopyAbility extends DiviWriteAbility
|
||||
}
|
||||
|
||||
$source = $this->loader->load($input['source_post_id'] ?? null, false);
|
||||
if ($source instanceof WP_Error) {
|
||||
return $source;
|
||||
}
|
||||
|
||||
return $source instanceof WP_Error ? $source : null;
|
||||
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
|
||||
|
||||
+8
-1
@@ -89,12 +89,19 @@ final class Plugin
|
||||
$libraryWritable = static function () use (&$policy): bool {
|
||||
return $policy instanceof ExposurePolicy && $policy->groupWritable(LibraryAccess::GROUP);
|
||||
};
|
||||
// Audit L5: lit/divi-node-copy from a library entry follows lit/library-insert's Tools-tab state.
|
||||
$libraryInsertable = static function () use (&$registry, &$policy): bool {
|
||||
return $registry instanceof IntegrationRegistry
|
||||
&& $policy instanceof ExposurePolicy
|
||||
&& $policy->allows('lit/library-insert', $registry->abilities());
|
||||
};
|
||||
$divi = new DiviIntegration(
|
||||
$schema,
|
||||
$changeLog,
|
||||
$libraryWritable,
|
||||
libraryVisible: $libraryVisible,
|
||||
diviWritable: $diviWritable
|
||||
diviWritable: $diviWritable,
|
||||
libraryInsertable: $libraryInsertable
|
||||
);
|
||||
$registry = new IntegrationRegistry(
|
||||
new CoreIntegration(self::pluginDir() . '/docs/guides'),
|
||||
|
||||
@@ -42,7 +42,8 @@ final class DiviNodeCopyAbilityTest extends TestCase
|
||||
private function ability(
|
||||
bool $diviWritable = true,
|
||||
bool $unfiltered = false,
|
||||
?LibraryAccess $access = null
|
||||
?LibraryAccess $access = null,
|
||||
bool $libraryInsert = true
|
||||
): DiviNodeCopyAbility {
|
||||
$access ??= LibraryFixtures::access();
|
||||
$loader = LibraryFixtures::loader($access);
|
||||
@@ -54,7 +55,8 @@ final class DiviNodeCopyAbilityTest extends TestCase
|
||||
return new DiviNodeCopyAbility(
|
||||
DiviWriterFixture::writer($this->saved, $failures, $unfiltered, $loader, $globals),
|
||||
$loader,
|
||||
static fn (): bool => $diviWritable
|
||||
static fn (): bool => $diviWritable,
|
||||
static fn (): bool => $libraryInsert
|
||||
);
|
||||
}
|
||||
|
||||
@@ -206,6 +208,49 @@ final class DiviNodeCopyAbilityTest extends TestCase
|
||||
self::assertStringContainsString('Library-Blurb', $this->saved[0]);
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
private function librarySource(): array
|
||||
{
|
||||
return $this->input([
|
||||
'source_post_id' => 1055,
|
||||
'source_tree_rev' => Rev::of((string) LibraryFixtures::item(1055)->post_content),
|
||||
'source_id' => 's0',
|
||||
]);
|
||||
}
|
||||
|
||||
public function testALibrarySourceNeedsAddLibrary(): void
|
||||
{
|
||||
$ability = $this->ability(access: LibraryFixtures::access(['add_library']));
|
||||
|
||||
self::assertToolError($ability->permission($this->librarySource()), 'forbidden');
|
||||
self::assertToolError($ability->execute($this->librarySource()), 'forbidden');
|
||||
self::assertSame([], $this->saved);
|
||||
}
|
||||
|
||||
public function testALibrarySourceNeedsAWritableLibraryGroup(): void
|
||||
{
|
||||
$ability = $this->ability(access: LibraryFixtures::access(writable: false));
|
||||
|
||||
self::assertToolError($ability->execute($this->librarySource()), 'forbidden');
|
||||
self::assertSame([], $this->saved);
|
||||
}
|
||||
|
||||
public function testALibrarySourceFollowsLibraryInsertExposure(): void
|
||||
{
|
||||
$ability = $this->ability(libraryInsert: false);
|
||||
|
||||
self::assertToolError($ability->permission($this->librarySource()), 'unsupported');
|
||||
self::assertToolError($ability->execute($this->librarySource()), 'unsupported');
|
||||
self::assertSame([], $this->saved);
|
||||
}
|
||||
|
||||
public function testPageSourcesNeedNeitherAddLibraryNorLibraryInsert(): void
|
||||
{
|
||||
$ability = $this->ability(access: LibraryFixtures::access(['add_library']), libraryInsert: false);
|
||||
|
||||
self::assertTrue($ability->execute($this->input())['ok']);
|
||||
}
|
||||
|
||||
public function testAHiddenLibraryIsNoSource(): void
|
||||
{
|
||||
$item = LibraryFixtures::item(1055);
|
||||
|
||||
Reference in New Issue
Block a user