diff --git a/src/Integrations/Divi/Design/PageRewriter.php b/src/Integrations/Divi/Design/PageRewriter.php index 9cb0296..1f66425 100644 --- a/src/Integrations/Divi/Design/PageRewriter.php +++ b/src/Integrations/Divi/Design/PageRewriter.php @@ -40,9 +40,10 @@ defined('ABSPATH') || exit; * that is dropped afterwards (audit L1). * - Skipped (named, never written): posts loadWritable() refuses (rights, Divi 4, unparsable markup – R8: not * remaining, so repeated calls end), global instances the change concerns (their library entry is a usage of its - * own), library entries shown on live posts the caller may not edit (P8a), and library entries or Theme Builder - * layouts visible site-wide without revisions (no restore point; a bulk write makes no backup copies). Posts the - * caller may not read are never named, only counted (hidden_count, audit L4). + * own; its ID is named only when DiviPageLoader::libraryIdsVisible(), ruling P13), library entries shown on + * live posts the caller may not edit (P8a), and library entries or Theme Builder layouts visible site-wide + * without revisions (no restore point; a bulk write makes no backup copies). Posts the caller may not read are + * never named, only counted (hidden_count, audit L4). * - Save scope as in DiviWriter. The rewrite changes attributes only, so a library entry keeps its shape * (LibraryContent::shapeViolation() needs no check). * - A failed save after other posts were saved answers partial_write with data.saved; a failure before the first @@ -232,7 +233,7 @@ class PageRewriter return null; } - $result = (new TreeRewrite($rewrite()))->apply($page->index); + $result = (new TreeRewrite($rewrite(), $this->loader->libraryIdsVisible()))->apply($page->index); foreach ($result['skipped'] as $node) { $skipped[] = ['post_id' => $id, 'reason' => $node['id'] . ': ' . $node['reason']]; } diff --git a/src/Integrations/Divi/Design/TreeRewrite.php b/src/Integrations/Divi/Design/TreeRewrite.php index 86f068c..8be25b3 100644 --- a/src/Integrations/Divi/Design/TreeRewrite.php +++ b/src/Integrations/Divi/Design/TreeRewrite.php @@ -14,11 +14,13 @@ defined('ABSPATH') || exit; * Applies a NodeRewrite to every block of a NodeIndex, in place (ruling R8): readable blocks are offered to * rewrite(), a changed one gets its new attributes and then its notes; opaque blocks – global instances, blocks with * unreadable attributes – are never offered and never changed, but listed as skipped when the strategy says the - * change concerns them (a global module is edited in its library entry). Pure PHP. + * change concerns them (a global module is edited in its library entry). The reason names the library entry's ID + * only with $revealLibraryIds (ruling P13: the caller may see library IDs); otherwise it points to the Divi Builder + * (fail closed). Pure PHP. */ final class TreeRewrite { - public function __construct(private NodeRewrite $rewrite) + public function __construct(private NodeRewrite $rewrite, private bool $revealLibraryIds = false) { } @@ -37,7 +39,7 @@ final class TreeRewrite if ($block->isOpaque()) { if ($this->rewrite->concernsInstance($block)) { - $skipped[] = ['id' => $id, 'reason' => self::reason($block)]; + $skipped[] = ['id' => $id, 'reason' => $this->reason($block)]; } continue; } @@ -55,11 +57,13 @@ final class TreeRewrite return ['changed' => $changed, 'warnings' => $warnings, 'skipped' => $skipped]; } - private static function reason(Block $block): string + private function reason(Block $block): string { $ref = NodeSummary::globalRef($block); if ($ref !== null) { - return sprintf('globales Modul – im Bibliothekseintrag %s ändern', $ref['id']); + return $this->revealLibraryIds + ? sprintf('globales Modul – im Bibliothekseintrag %s ändern', $ref['id']) + : 'globales Modul – im Divi-Builder ändern'; } return $block->isGlobal() diff --git a/tests/Unit/Integrations/Divi/Design/PageRewriterTest.php b/tests/Unit/Integrations/Divi/Design/PageRewriterTest.php index 5f1281a..a22b3f3 100644 --- a/tests/Unit/Integrations/Divi/Design/PageRewriterTest.php +++ b/tests/Unit/Integrations/Divi/Design/PageRewriterTest.php @@ -14,6 +14,7 @@ use LIT\MCP\Integrations\Divi\Design\PageRewriter; use LIT\MCP\Integrations\Divi\Library\GlobalUsages; use LIT\MCP\Integrations\Divi\Library\UsageQuery; use LIT\MCP\Integrations\Divi\Library\UsageReport; +use LIT\MCP\Integrations\Divi\Pages\DiviPageLoader; use LIT\MCP\Integrations\Divi\Pages\DiviSaver; use LIT\MCP\Integrations\Divi\Pages\SaveScope; use LIT\MCP\Integrations\Divi\Release\DiviLocked; @@ -94,13 +95,18 @@ final class PageRewriterTest extends TestCase /** * The rewriter over $posts: they are the candidates (ID order) and what get_post() finds; the caller has every - * capability. $report answers reportFor() (default: all posts, nothing blocked), $libraryReports report($id). + * capability. $report answers reportFor() (default: all posts, nothing blocked), $libraryReports report($id); + * $loader defaults to one whose user may see library IDs. * * @param list $posts * @param array $libraryReports */ - private function rewriter(array $posts, ?UsageReport $report = null, array $libraryReports = []): PageRewriter - { + private function rewriter( + array $posts, + ?UsageReport $report = null, + array $libraryReports = [], + ?DiviPageLoader $loader = null + ): PageRewriter { LibraryFixtures::given($posts); $rows = array_map( static fn (WP_Post $post): array => [ @@ -117,7 +123,7 @@ final class PageRewriterTest extends TestCase static fn (int $id): UsageReport => $libraryReports[$id] ?? self::report(0) ); - return new PageRewriter($usages, LibraryFixtures::loader(), $this->saver(), $this->log); + return new PageRewriter($usages, $loader ?? LibraryFixtures::loader(), $this->saver(), $this->log); } /** Records post ID and scope, puts the content on the post and reports revision 900 + n. */ @@ -454,6 +460,38 @@ final class PageRewriterTest extends TestCase self::assertStringContainsString('"modulePreset":["p-cta"]', (string) $post->post_content); } + /** Ruling P13: a user who may see library IDs is told which entry to edit. */ + public function testGlobalInstanceReasonNamesTheEntryWhenIdsAreVisible(): void + { + $rewriter = $this->rewriter([self::page(101, self::layout(self::INSTANCE, self::BUTTON))]); + + $preview = $rewriter->preview(self::query(), self::assign(), null); + $result = $rewriter->apply(self::ABILITY, self::query(), self::assign(), null, self::SUMMARY); + + self::assertSame( + 's0.r0.c0.g0: globales Modul – im Bibliothekseintrag 1059 ändern', + $preview['skipped'][0]['reason'] + ); + self::assertSame($preview['skipped'], $result['skipped']); + } + + /** Ruling P13: without the library right the reason names no library ID (dry run and write). */ + public function testGlobalInstanceReasonHidesTheEntryWithoutLibraryRights(): void + { + $loader = LibraryFixtures::loader(LibraryFixtures::access(['divi_library'])); + $rewriter = $this->rewriter([self::page(101, self::layout(self::INSTANCE, self::BUTTON))], null, [], $loader); + self::assertFalse($loader->libraryIdsVisible()); + + $preview = $rewriter->preview(self::query(), self::assign(), null); + $result = $rewriter->apply(self::ABILITY, self::query(), self::assign(), null, self::SUMMARY); + + $expected = [['post_id' => 101, 'reason' => 's0.r0.c0.g0: globales Modul – im Divi-Builder ändern']]; + self::assertSame($expected, $preview['skipped']); + self::assertSame($expected, $result['skipped']); + self::assertSame([101], array_column($result['saved'], 'post_id')); + self::assertStringNotContainsString('1059', (string) json_encode([$preview, $result])); + } + /** R8: library entries are usages of their own, saved with DiviWriter's scope. */ public function testLibraryEntriesAreSavedWithTheirScope(): void { diff --git a/tests/Unit/Integrations/Divi/Design/TreeRewriteTest.php b/tests/Unit/Integrations/Divi/Design/TreeRewriteTest.php index bcfabf3..e2e1809 100644 --- a/tests/Unit/Integrations/Divi/Design/TreeRewriteTest.php +++ b/tests/Unit/Integrations/Divi/Design/TreeRewriteTest.php @@ -86,7 +86,7 @@ final class TreeRewriteTest extends TestCase ); $before = Serializer::document($document); - $result = (new TreeRewrite(self::touchButtons()))->apply(NodeIndex::build($document)); + $result = (new TreeRewrite(self::touchButtons(), true))->apply(NodeIndex::build($document)); self::assertSame([], $result['changed']); self::assertSame( @@ -96,6 +96,21 @@ final class TreeRewriteTest extends TestCase self::assertSame($before, Serializer::document($document), 'instances are never rewritten'); } + /** Ruling P13, fail closed: without $revealLibraryIds the reason names no library ID. */ + public function testGlobalInstanceReasonHidesTheLibraryIdByDefault(): void + { + $document = self::document( + '' + ); + + $result = (new TreeRewrite(self::touchButtons()))->apply(NodeIndex::build($document)); + + self::assertSame( + [['id' => 's0.r0.c0.g0', 'reason' => 'globales Modul – im Divi-Builder ändern']], + $result['skipped'] + ); + } + public function testBlocksWithUnreadableAttributesAreSkippedToo(): void { $document = self::document('');