diff --git a/docs/guides/divi-library.md b/docs/guides/divi-library.md index cec9b81..ba3c121 100644 --- a/docs/guides/divi-library.md +++ b/docs/guides/divi-library.md @@ -53,7 +53,9 @@ Die Divi-Bibliothek speichert Layouts, Abschnitte, Zeilen und Module zur Wiederv Die Bestätigung wird abgelehnt (`forbidden`), wenn das Konto einen veröffentlichten, privaten oder geplanten Eintrag, der das Modul zeigt, nicht bearbeiten darf – schon der Probelauf warnt davor und nennt diese IDs in -`blocked_ids`, nicht lesbare nur als Anzahl in `blocked_hidden_count` (Entwürfe blockieren nicht). Zeigen +`blocked_ids`, nicht lesbare nur als Anzahl in `blocked_hidden_count` (Entwürfe blockieren nicht). +Theme-Builder-Layouts (Header, Body, Footer, Vorlagen) blockieren außerdem, wenn die Divi-Rolle des Kontos +„Theme Builder“ nicht erlaubt. Zeigen mehr als 2000 veröffentlichte, private oder geplante Einträge das Modul, lehnt LIT MCP die Bestätigung ab (`unsupported`); solche Einträge im Divi-Builder bearbeiten. Sind Revisionen für die Bibliothek abgeschaltet, lehnt das Werkzeug die Änderung außerdem ab (`unsupported`), wenn der Papierkorb deaktiviert ist oder das Konto kein diff --git a/src/Integrations/Divi/DiviIntegration.php b/src/Integrations/Divi/DiviIntegration.php index 1ff9cbb..2702d5f 100644 --- a/src/Integrations/Divi/DiviIntegration.php +++ b/src/Integrations/Divi/DiviIntegration.php @@ -155,7 +155,7 @@ final class DiviIntegration implements Integration $this->log, $this->design, new DiviComposition($this->modules, $this->design), - new GlobalEdits($this->access, new GlobalUsages()) + new GlobalEdits($this->access, new GlobalUsages(null, $this->access->roles())) ); } diff --git a/src/Integrations/Divi/Library/DiviRoles.php b/src/Integrations/Divi/Library/DiviRoles.php index 1adc241..a28c9f0 100644 --- a/src/Integrations/Divi/Library/DiviRoles.php +++ b/src/Integrations/Divi/Library/DiviRoles.php @@ -25,6 +25,7 @@ class DiviRoles 'add_library' => 'Aus der Bibliothek hinzufügen', 'edit_global_library' => 'Globale Bibliothekselemente bearbeiten', 'read_dynamic_content_custom_fields' => 'Dynamic Content: benutzerdefinierte Felder lesen', + 'theme_builder' => 'Theme Builder', ]; /** @param (Closure(string): bool)|null $check Test seam; null = Divi's et_pb_is_allowed(). */ diff --git a/src/Integrations/Divi/Library/GlobalUsages.php b/src/Integrations/Divi/Library/GlobalUsages.php index 7f385ae..3481c37 100644 --- a/src/Integrations/Divi/Library/GlobalUsages.php +++ b/src/Integrations/Divi/Library/GlobalUsages.php @@ -15,7 +15,8 @@ defined('ABSPATH') || exit; * audit L1); the rights of the caller are then checked in chunks with a bounded post cache: * - readable posts are listed (at most LIMIT, with title); unreadable ones only count (audit L4); * - live posts (ruling P8a) the caller may not edit_post block a confirmed edit – by ID when - * readable, else as a count; above HARD_CAP live posts the edit is refused as a whole. + * readable, else as a count; Theme Builder layouts also without Divi's theme_builder role option + * (audit L6); above HARD_CAP live posts the edit is refused as a whole. * Not final so tests can mock rows(). */ class GlobalUsages @@ -38,8 +39,14 @@ class GlobalUsages /** Every post that exists for the user: listed in the dry run. */ private const LISTED = "post_status NOT IN ('trash', 'auto-draft', 'inherit')"; - public function __construct(private ?\wpdb $db = null) + /** Theme Builder post types (Divi's et_theme_builder_get_layout_post_types() plus the templates). */ + public const THEME_BUILDER_TYPES = ['et_header_layout', 'et_body_layout', 'et_footer_layout', 'et_template']; + + private DiviRoles $roles; + + public function __construct(private ?\wpdb $db = null, ?DiviRoles $roles = null) { + $this->roles = $roles ?? new DiviRoles(); } /** @@ -142,12 +149,18 @@ class GlobalUsages } /** - * Ruling P8a: whether the caller may edit_post the live post $row. + * Ruling P8a: whether the caller may edit_post the live post $row; a Theme Builder layout or + * template additionally needs Divi's role option theme_builder (audit L6 – as Divi's own + * et_pb_is_allowed check for TB layouts, builder/functions.php). * * @param array{id: int, status: string, type: string} $row */ - protected function editable(array $row): bool + private function editable(array $row): bool { + if (in_array($row['type'], self::THEME_BUILDER_TYPES, true) && ! $this->roles->allows('theme_builder')) { + return false; + } + return current_user_can('edit_post', $row['id']); } diff --git a/src/Integrations/Divi/Library/LibraryIntegration.php b/src/Integrations/Divi/Library/LibraryIntegration.php index c32fd32..1b2d750 100644 --- a/src/Integrations/Divi/Library/LibraryIntegration.php +++ b/src/Integrations/Divi/Library/LibraryIntegration.php @@ -54,7 +54,12 @@ final class LibraryIntegration implements Integration public function abilities(): array { return [ - new LibraryListAbility($this->access, $this->loader, $this->design, new GlobalUsages()), + new LibraryListAbility( + $this->access, + $this->loader, + $this->design, + new GlobalUsages(null, $this->access->roles()) + ), new LibrarySaveAbility( $this->access, $this->loader, diff --git a/tests/Support/LibraryFixtures.php b/tests/Support/LibraryFixtures.php index 83742fb..19b204e 100644 --- a/tests/Support/LibraryFixtures.php +++ b/tests/Support/LibraryFixtures.php @@ -123,7 +123,7 @@ final class LibraryFixtures Functions\when('_get_non_cached_ids')->returnArg(1); Functions\when('_prime_post_caches')->justReturn(null); Functions\when('wp_cache_delete')->justReturn(true); - $usages = Mockery::mock(GlobalUsages::class, [null, $roles])->makePartial(); + $usages = Mockery::mock(GlobalUsages::class, [null, $roles ?? self::roles()])->makePartial(); $usages->allows('rows')->andReturnUsing(static fn (): array => is_array($rows) ? $rows : $rows()); return $usages; diff --git a/tests/Unit/Integrations/Divi/Library/GlobalUsagesTest.php b/tests/Unit/Integrations/Divi/Library/GlobalUsagesTest.php index 5e17674..a682cd9 100644 --- a/tests/Unit/Integrations/Divi/Library/GlobalUsagesTest.php +++ b/tests/Unit/Integrations/Divi/Library/GlobalUsagesTest.php @@ -124,6 +124,26 @@ final class GlobalUsagesTest extends TestCase self::assertSame(GlobalUsages::CHECK_CAP, $asked['read_post']); } + /** Audit L6: Theme Builder layouts additionally need Divi's role option theme_builder. */ + public function testThemeBuilderLayoutsNeedTheThemeBuilderRole(): void + { + $rows = [ + ['id' => 1053, 'status' => 'publish', 'type' => 'page'], + ['id' => 1061, 'status' => 'publish', 'type' => 'et_header_layout'], + ['id' => 1062, 'status' => 'publish', 'type' => 'et_body_layout'], + ['id' => 1063, 'status' => 'publish', 'type' => 'et_footer_layout'], + ['id' => 1064, 'status' => 'publish', 'type' => 'et_template'], + ]; + Functions\when('get_post')->justReturn(null); + Functions\when('current_user_can')->justReturn(true); + + $without = LibraryFixtures::usages($rows, LibraryFixtures::roles(['theme_builder']))->report(1058); + $with = LibraryFixtures::usages($rows, LibraryFixtures::roles())->report(1058); + + self::assertSame([1061, 1062, 1063, 1064], $without->blocked); + self::assertSame([], $with->blocked); + } + public function testAListingSkipsTheEditChecks(): void { $usages = LibraryFixtures::usages(LibraryFixtures::rows([1053]));