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:
@@ -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
|
||||||
|
|||||||
@@ -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(). */
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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]));
|
||||||
|
|||||||
Reference in New Issue
Block a user