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:
@@ -23,8 +23,11 @@ final class Outline
|
||||
|
||||
private bool $truncated = false;
|
||||
|
||||
/** @param array<string, string> $presetNames Preset ID => name. */
|
||||
public function __construct(private array $presetNames = [])
|
||||
/**
|
||||
* @param array<string, string> $presetNames Preset ID => name.
|
||||
* @param bool $globalIds Ruling P13: false leaves out the library IDs of global instances.
|
||||
*/
|
||||
public function __construct(private array $presetNames = [], private bool $globalIds = true)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -157,7 +160,7 @@ final class Outline
|
||||
'editable' => $editable,
|
||||
'depth' => $depth,
|
||||
'truncated' => false,
|
||||
'global_id' => $global['id'] ?? null,
|
||||
'global_id' => $this->globalIds ? ($global['id'] ?? null) : null,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,6 +79,12 @@ final class DiviIntegration implements Integration
|
||||
return 'divi';
|
||||
}
|
||||
|
||||
/** Ruling P13: whether this connection may see library IDs (library group visible, library right). */
|
||||
public function libraryIdsVisible(): bool
|
||||
{
|
||||
return $this->access->revealsIds();
|
||||
}
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return __('Divi', 'wp-lit-mcp');
|
||||
|
||||
@@ -80,6 +80,17 @@ class LibraryAccess
|
||||
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
|
||||
{
|
||||
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\WordPress\Render;
|
||||
|
||||
use Closure;
|
||||
use LIT\MCP\Abilities\Ability;
|
||||
use LIT\MCP\Abilities\Annotations;
|
||||
use LIT\MCP\Integrations\WordPress\PostGuard;
|
||||
@@ -22,8 +23,15 @@ final class PageRenderSummaryAbility implements Ability
|
||||
/** At most this many global-module notes per summary. */
|
||||
private const GLOBAL_NOTES = 20;
|
||||
|
||||
public function __construct(private ContentSchema $schema, private PageRenderer $renderer)
|
||||
{
|
||||
/**
|
||||
* @param (Closure(): bool)|null $globalIdsVisible Ruling P13: whether global module IDs may be named
|
||||
* (library access of this connection); null = always.
|
||||
*/
|
||||
public function __construct(
|
||||
private ContentSchema $schema,
|
||||
private PageRenderer $renderer,
|
||||
private ?Closure $globalIdsVisible = null
|
||||
) {
|
||||
}
|
||||
|
||||
public function name(): string
|
||||
@@ -108,13 +116,23 @@ final class PageRenderSummaryAbility implements Ability
|
||||
$notes[] = 'Der Eintrag ist passwortgeschützt; Besucher sehen zuerst das Passwortformular.';
|
||||
}
|
||||
|
||||
foreach (array_slice(self::globalModuleIds((string) $post->post_content), 0, self::GLOBAL_NOTES) as $id) {
|
||||
$globalIds = self::globalModuleIds((string) $post->post_content);
|
||||
if ($globalIds !== [] && $this->globalIdsVisible !== null && ! ($this->globalIdsVisible)()) {
|
||||
$notes[] = sprintf(
|
||||
'Globales Modul %s ist im Auszug nicht enthalten (beim Aufruf über die Schnittstelle rendert Divi '
|
||||
. 'globale Module nicht); Inhalt mit lit-divi-outline mit post_id %s lesen.',
|
||||
$id,
|
||||
$id
|
||||
'%d %s im Auszug nicht enthalten (beim Aufruf über die Schnittstelle rendert Divi globale Module '
|
||||
. 'nicht; die Divi-Bibliothek ist für dieses Konto nicht zugänglich).',
|
||||
count($globalIds),
|
||||
count($globalIds) === 1 ? 'globales Modul ist' : 'globale Module sind'
|
||||
);
|
||||
} else {
|
||||
foreach (array_slice($globalIds, 0, self::GLOBAL_NOTES) as $id) {
|
||||
$notes[] = sprintf(
|
||||
'Globales Modul %s ist im Auszug nicht enthalten (beim Aufruf über die Schnittstelle rendert Divi '
|
||||
. 'globale Module nicht); Inhalt mit lit-divi-outline mit post_id %s lesen.',
|
||||
$id,
|
||||
$id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return array_merge(
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\WordPress;
|
||||
|
||||
use Closure;
|
||||
use LIT\MCP\ChangeLog\ChangeLog;
|
||||
use LIT\MCP\Integrations\Integration;
|
||||
use LIT\MCP\Integrations\WordPress\Content\ContentComposer;
|
||||
@@ -21,8 +22,8 @@ use LIT\MCP\Integrations\WordPress\Menus\MenuListAbility;
|
||||
use LIT\MCP\Integrations\WordPress\Menus\MenuOperations;
|
||||
use LIT\MCP\Integrations\WordPress\Menus\MenuSetItemsAbility;
|
||||
use LIT\MCP\Integrations\WordPress\Menus\MenuWriter;
|
||||
use LIT\MCP\Integrations\WordPress\Render\PageRenderer;
|
||||
use LIT\MCP\Integrations\WordPress\Render\PageRenderSummaryAbility;
|
||||
use LIT\MCP\Integrations\WordPress\Render\PageRenderer;
|
||||
use LIT\MCP\Integrations\WordPress\Revisions\RevisionRestoreAbility;
|
||||
use LIT\MCP\Integrations\WordPress\Revisions\RevisionsListAbility;
|
||||
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
|
||||
@@ -49,10 +50,15 @@ defined('ABSPATH') || exit;
|
||||
final class WordPressIntegration implements Integration
|
||||
{
|
||||
/** @param ContentComposer|null $divi Divi short form for lit/content-create (DiviIntegration). */
|
||||
/**
|
||||
* @param (Closure(): bool)|null $globalIdsVisible Ruling P13: whether lit-page-render-summary may name
|
||||
* global module IDs (the Divi integration's library access); null = always.
|
||||
*/
|
||||
public function __construct(
|
||||
private ContentSchema $schema,
|
||||
private ChangeLog $log,
|
||||
private ?ContentComposer $divi = null
|
||||
private ?ContentComposer $divi = null,
|
||||
private ?Closure $globalIdsVisible = null
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -99,7 +105,7 @@ final class WordPressIntegration implements Integration
|
||||
new MenuAssignLocationAbility($this->log),
|
||||
new WpSettingsGetAbility($whitelist),
|
||||
new WpSettingsUpdateAbility($whitelist, $this->log),
|
||||
new PageRenderSummaryAbility($this->schema, new PageRenderer()),
|
||||
new PageRenderSummaryAbility($this->schema, new PageRenderer(), $this->globalIdsVisible),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
+6
-1
@@ -105,7 +105,12 @@ final class Plugin
|
||||
);
|
||||
$registry = new IntegrationRegistry(
|
||||
new CoreIntegration(self::pluginDir() . '/docs/guides'),
|
||||
new WordPressIntegration($schema, $changeLog, $divi->contentComposer($diviWritable)),
|
||||
new WordPressIntegration(
|
||||
$schema,
|
||||
$changeLog,
|
||||
$divi->contentComposer($diviWritable),
|
||||
$divi->libraryIdsVisible(...)
|
||||
),
|
||||
new MediaIntegration($schema, $changeLog, $limits),
|
||||
$divi,
|
||||
$divi->library()
|
||||
|
||||
Reference in New Issue
Block a user