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>
This commit is contained in:
2026-09-27 15:00:39 +02:00
co-authored by Claude Opus 5.5
parent 06f13799de
commit c7ce924e5a
19 changed files with 351 additions and 28 deletions
@@ -130,13 +130,22 @@ final class DiviNodeGetAbility implements Ability
$version = $block->attrs()['builderVersion'] ?? null;
$global = NodeSummary::globalRef($block);
if ($global !== null) {
if ($global !== null && $this->loader->libraryIdsVisible()) {
$global['hint'] = sprintf(
'Globales Modul: Inhalt im Bibliothekseintrag %s bearbeiten (lit-divi-outline mit post_id %s); '
. 'jede Änderung dort wirkt auf alle Seiten mit diesem Modul.',
$global['id'],
$global['id']
);
} elseif ($global !== null) {
// Ruling P13: which entry is shown stays unknown without library access (AttrMap::flatten
// already leaves globalModule/globalParent out of attrs).
$global = [
'id' => null,
'block_name' => $global['block_name'],
'hint' => 'Globales Modul aus der Divi-Bibliothek – nur lesbar; dieses Konto hat keinen Zugang zur '
. 'Divi-Bibliothek.',
];
}
return [
@@ -153,7 +153,7 @@ final class DiviOutlineAbility implements Ability
return DiviErrors::unknownNode($rootId, $page->post);
}
$outline = new Outline($this->design->read()->presetNames());
$outline = new Outline($this->design->read()->presetNames(), $this->loader->libraryIdsVisible());
$rows = $outline->rows(
$page->index,
$rootId,
@@ -191,8 +191,12 @@ final class DiviOutlineAbility implements Ability
if (in_array(false, array_column($rows, 'editable'), true)) {
$hints[] = 'Knoten mit "nur lesbar" (globale Module, fremde Blöcke) lassen sich nur als Ganzes '
. 'verschieben, duplizieren oder löschen. Globale Module [global <ID>] im Bibliothekseintrag '
. 'bearbeiten: lit-divi-outline mit post_id <ID> – die Änderung wirkt auf alle Seiten.';
. 'verschieben, duplizieren oder löschen. '
. ($this->loader->libraryIdsVisible()
? 'Globale Module [global <ID>] im Bibliothekseintrag bearbeiten: lit-divi-outline mit post_id '
. '<ID> – die Änderung wirkt auf alle Seiten.'
// Ruling P13: without library access the entry stays unnamed.
: 'Globale Module stammen aus der Divi-Bibliothek, für die dieses Konto keinen Zugang hat.');
}
return $hints;
@@ -41,6 +41,12 @@ final class DiviPageLoader
return $this->library;
}
/** Ruling P13: whether the Divi tools may name library IDs (no library access = never). */
public function libraryIdsVisible(): bool
{
return $this->library?->revealsIds() ?? false;
}
/** Readers pass $forWrite = false (trashed entries may be read, like lit-content-get). */
public function load(mixed $postId, bool $forWrite): DiviPage|WP_Error
{
@@ -417,6 +417,11 @@ final class DiviReplaceTextAbility implements Ability
*/
private function hits(DiviPage $page, TextReplacer $replacer): array
{
return $this->search->replace($page, $replacer, $this->writer->composition($page)->cleaner());
return $this->search->replace(
$page,
$replacer,
$this->writer->composition($page)->cleaner(),
$this->loader->libraryIdsVisible()
);
}
}
+27 -4
View File
@@ -68,14 +68,20 @@ final class DiviTextSearch
}
/**
* Every hit of $page, computed without changing it.
* Every hit of $page, computed without changing it. Without $globalIds (ruling P13: no library
* access) the library IDs a global instance stores are no text to find: an opaque node is
* matched without globalModule/globalParent, as if they were not stored.
*
* @return array{count: int, samples: list<array<string, string>>,
* blocks: list<array{Block, array<string, mixed>}>, skipped_nodes: list<array{id: string, reason: string}>}
* @throws InvalidArgumentException When a place would be changed unsafely – the page fails.
*/
public function replace(DiviPage $page, TextReplacer $replacer, ValueCleaner $cleaner): array
{
public function replace(
DiviPage $page,
TextReplacer $replacer,
ValueCleaner $cleaner,
bool $globalIds = true
): array {
$count = 0;
$samples = [];
$blocks = [];
@@ -83,7 +89,8 @@ final class DiviTextSearch
foreach ($page->index->entries() as $entry) {
$block = $entry['block'];
if ($block->isDivi() && $block->isOpaque()) {
if ($replacer->mentions(Serializer::block($block))) {
$markup = $globalIds ? Serializer::block($block) : self::withoutLibraryIds($block);
if ($replacer->mentions($markup)) {
$skipped[] = ['id' => (string) $entry['id'], 'reason' => self::opaqueReason($block)];
}
continue;
@@ -112,6 +119,22 @@ final class DiviTextSearch
return ['count' => $count, 'samples' => $samples, 'blocks' => $blocks, 'skipped_nodes' => $skipped];
}
/**
* Ruling P13: $block's markup with every globalModule/globalParent left out, at every depth –
* only for matching, never saved.
*/
private static function withoutLibraryIds(Block $block): string
{
$attrs = $block->attrs();
unset($attrs['globalModule'], $attrs['globalParent']);
$markup = '<!-- wp:' . $block->name() . ' ' . Serializer::attributes($attrs) . ' -->';
foreach ($block->isVoid() ? [] : $block->parts() as $part) {
$markup .= $part instanceof Block ? self::withoutLibraryIds($part) : $part;
}
return $markup . '<!-- /wp:' . $block->name() . ' -->';
}
/** Why an opaque node is not changed (global modules are edited in the Divi library, Etappe 3). */
private static function opaqueReason(Block $block): string
{
+1 -1
View File
@@ -247,7 +247,7 @@ final class DiviWriter
return ['text' => null, 'truncated' => false];
}
$outline = new Outline($this->design->read()->presetNames());
$outline = new Outline($this->design->read()->presetNames(), $this->loader->libraryIdsVisible());
return $outline->summary($index, array_map('strval', array_keys($sections)));
}