fix: P8a blocks Theme Builder layouts without Divi's theme_builder role (L6)

et_header_layout, et_body_layout, et_footer_layout and et_template showing a
global entry now block a confirmed edit unless et_pb_is_allowed('theme_builder'),
like Divi's own check for TB layouts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-27 04:13:14 +02:00
co-authored by Claude Opus 5.5
parent e8f69f67ea
commit 8aacc5dc4f
7 changed files with 49 additions and 8 deletions
+3 -1
View File
@@ -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 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 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 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 (`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 Werkzeug die Änderung außerdem ab (`unsupported`), wenn der Papierkorb deaktiviert ist oder das Konto kein
+1 -1
View File
@@ -155,7 +155,7 @@ final class DiviIntegration implements Integration
$this->log, $this->log,
$this->design, $this->design,
new DiviComposition($this->modules, $this->design), new DiviComposition($this->modules, $this->design),
new GlobalEdits($this->access, new GlobalUsages()) new GlobalEdits($this->access, new GlobalUsages(null, $this->access->roles()))
); );
} }
@@ -25,6 +25,7 @@ class DiviRoles
'add_library' => 'Aus der Bibliothek hinzufügen', 'add_library' => 'Aus der Bibliothek hinzufügen',
'edit_global_library' => 'Globale Bibliothekselemente bearbeiten', 'edit_global_library' => 'Globale Bibliothekselemente bearbeiten',
'read_dynamic_content_custom_fields' => 'Dynamic Content: benutzerdefinierte Felder lesen', '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(). */ /** @param (Closure(string): bool)|null $check Test seam; null = Divi's et_pb_is_allowed(). */
+17 -4
View File
@@ -15,7 +15,8 @@ defined('ABSPATH') || exit;
* audit L1); the rights of the caller are then checked in chunks with a bounded post cache: * 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); * - 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 * - 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(). * Not final so tests can mock rows().
*/ */
class GlobalUsages class GlobalUsages
@@ -38,8 +39,14 @@ class GlobalUsages
/** Every post that exists for the user: listed in the dry run. */ /** Every post that exists for the user: listed in the dry run. */
private const LISTED = "post_status NOT IN ('trash', 'auto-draft', 'inherit')"; 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 * @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']); return current_user_can('edit_post', $row['id']);
} }
@@ -54,7 +54,12 @@ final class LibraryIntegration implements Integration
public function abilities(): array public function abilities(): array
{ {
return [ 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( new LibrarySaveAbility(
$this->access, $this->access,
$this->loader, $this->loader,
+1 -1
View File
@@ -123,7 +123,7 @@ final class LibraryFixtures
Functions\when('_get_non_cached_ids')->returnArg(1); Functions\when('_get_non_cached_ids')->returnArg(1);
Functions\when('_prime_post_caches')->justReturn(null); Functions\when('_prime_post_caches')->justReturn(null);
Functions\when('wp_cache_delete')->justReturn(true); 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()); $usages->allows('rows')->andReturnUsing(static fn (): array => is_array($rows) ? $rows : $rows());
return $usages; return $usages;
@@ -124,6 +124,26 @@ final class GlobalUsagesTest extends TestCase
self::assertSame(GlobalUsages::CHECK_CAP, $asked['read_post']); 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 public function testAListingSkipsTheEditChecks(): void
{ {
$usages = LibraryFixtures::usages(LibraryFixtures::rows([1053])); $usages = LibraryFixtures::usages(LibraryFixtures::rows([1053]));