fix: global usages follow the Theme Builder access (ruling Q17 M1)
GlobalUsages gets the Theme Builder access injected as a closure (ThemeBuilderAccess::usageState(), wired at both construction sites through DiviIntegration::globalUsages()). Theme Builder posts are named only while the Theme Builder is visible to the account, otherwise only counted (also in blocked_ids); a global edit that changes them needs canUse(), a writable "Divi Theme Builder" group and edit_post. Without an injected access the fail-safe default checks what canUse() checks. The duplicate THEME_BUILDER_TYPES list gives way to DiviThemeBuilder::postTypes(). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
This commit is contained in:
@@ -81,9 +81,10 @@ 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).
|
`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-Layouts (Header, Body, Footer, Vorlagen) blockieren außerdem, wenn das Konto den Theme Builder
|
||||||
„Theme Builder“ nicht erlaubt. Zeigen
|
nicht benutzen darf (Divi-Rolle „Theme Builder“, `edit_others_posts`, `edit_theme_options`) oder die Gruppe
|
||||||
mehr als 2000 veröffentlichte, private oder geplante Einträge das Modul, lehnt LIT MCP die Bestätigung ab
|
„Divi Theme Builder“ auf „nur lesen“ steht; ohne Zugang zum Theme Builder erscheinen sie nur als Anzahl
|
||||||
|
(`hidden_count`, `blocked_hidden_count`), nie mit ID oder Titel. 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
|
(`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
|
||||||
`unfiltered_html` hat (die Sicherungskopie wäre dann nicht verlustfrei).
|
`unfiltered_html` hat (die Sicherungskopie wäre dann nicht verlustfrei).
|
||||||
|
|||||||
+2
-1
@@ -73,7 +73,8 @@ Einstellungen: Einstellungen → LIT MCP (Verbindung, Zugriff, Tools, Protokoll,
|
|||||||
verlangt dafür einen Probelauf mit Liste der betroffenen Seiten und confirm, legt vorher eine Revision (bei
|
verlangt dafür einen Probelauf mit Liste der betroffenen Seiten und confirm, legt vorher eine Revision (bei
|
||||||
abgeschalteten Revisionen eine Sicherungskopie im Papierkorb) an und leert danach Divis CSS-Cache aller Seiten
|
abgeschalteten Revisionen eine Sicherungskopie im Papierkorb) an und leert danach Divis CSS-Cache aller Seiten
|
||||||
und den Theme-Builder-Cache. Die Bestätigung wird abgelehnt, wenn der Benutzer einen veröffentlichten,
|
und den Theme-Builder-Cache. Die Bestätigung wird abgelehnt, wenn der Benutzer einen veröffentlichten,
|
||||||
privaten oder geplanten Eintrag, der das Modul zeigt, nicht bearbeiten darf.
|
privaten oder geplanten Eintrag, der das Modul zeigt, nicht bearbeiten darf – bei Theme-Builder-Layouts
|
||||||
|
auch ohne die Theme-Builder-Rechte; ohne Zugang zum Theme Builder nennt LIT MCP solche Layouts nur als Anzahl.
|
||||||
* Divis Rollen-Editor gilt zusätzlich zu den WordPress-Rechten: ohne „Divi-Bibliothek“, „In der Bibliothek
|
* Divis Rollen-Editor gilt zusätzlich zu den WordPress-Rechten: ohne „Divi-Bibliothek“, „In der Bibliothek
|
||||||
speichern“, „Aus der Bibliothek hinzufügen“ bzw. „Globale Bibliothekselemente bearbeiten“ lehnt LIT MCP ab.
|
speichern“, „Aus der Bibliothek hinzufügen“ bzw. „Globale Bibliothekselemente bearbeiten“ lehnt LIT MCP ab.
|
||||||
* Divi Theme Builder: neue Vorlagen legt LIT MCP deaktiviert an (über Divis eigene Funktionen, mit Layouts für
|
* Divi Theme Builder: neue Vorlagen legt LIT MCP deaktiviert an (über Divis eigene Funktionen, mit Layouts für
|
||||||
|
|||||||
@@ -170,10 +170,22 @@ final class DiviIntegration implements Integration
|
|||||||
$this->log,
|
$this->log,
|
||||||
$this->modules,
|
$this->modules,
|
||||||
$this->diviWritable,
|
$this->diviWritable,
|
||||||
$this->schema
|
$this->schema,
|
||||||
|
$this->globalUsages()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The usages of library entries with this integration's Theme Builder access (ruling Q17 M1, review I-2):
|
||||||
|
* Theme Builder posts are named only while ThemeBuilderAccess::visible(), and a global edit that changes them
|
||||||
|
* needs the rights of every Theme Builder write (canUse(), in visible()) and a writable "Divi Theme Builder"
|
||||||
|
* group – as the Divi tools on those layouts.
|
||||||
|
*/
|
||||||
|
public function globalUsages(?\wpdb $db = null): GlobalUsages
|
||||||
|
{
|
||||||
|
return new GlobalUsages($db, $this->access->roles(), $this->themeBuilderAccess->usageState(...));
|
||||||
|
}
|
||||||
|
|
||||||
/** The group "Divi Theme Builder" (lit/tb-*), sharing this integration's Theme Builder access (Etappe 3b). */
|
/** The group "Divi Theme Builder" (lit/tb-*), sharing this integration's Theme Builder access (Etappe 3b). */
|
||||||
public function themeBuilder(): ThemeBuilderIntegration
|
public function themeBuilder(): ThemeBuilderIntegration
|
||||||
{
|
{
|
||||||
@@ -194,7 +206,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(null, $this->access->roles())),
|
new GlobalEdits($this->access, $this->globalUsages()),
|
||||||
new ThemeBuilderEdits($this->themeBuilderAccess, fields: new DynamicFields($this->schema)),
|
new ThemeBuilderEdits($this->themeBuilderAccess, fields: new DynamicFields($this->schema)),
|
||||||
new SimilarSections($this->access)
|
new SimilarSections($this->access)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace LIT\MCP\Integrations\Divi\Library;
|
namespace LIT\MCP\Integrations\Divi\Library;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use LIT\MCP\Integrations\Divi\ThemeBuilder\DiviThemeBuilder;
|
||||||
use WP_Post;
|
use WP_Post;
|
||||||
|
|
||||||
defined('ABSPATH') || exit;
|
defined('ABSPATH') || exit;
|
||||||
@@ -16,8 +18,10 @@ defined('ABSPATH') || exit;
|
|||||||
* rights of the caller are then checked in chunks with a bounded post cache:
|
* 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; Theme Builder layouts also without Divi's theme_builder role option
|
* readable, else as a count; above HARD_CAP live posts the edit is refused as a whole;
|
||||||
* (audit L6); above HARD_CAP live posts the edit is refused as a whole.
|
* - Theme Builder posts follow the Theme Builder access (ruling Q17 M1, review I-2, audit L1/L6): readable
|
||||||
|
* only while the Theme Builder is visible to the caller (else counted, never named – P12, like
|
||||||
|
* ThemeBuilderAccess), editable only while the caller may use it (canUse()) plus edit_post.
|
||||||
* Not final so tests can mock rows().
|
* Not final so tests can mock rows().
|
||||||
*/
|
*/
|
||||||
class GlobalUsages
|
class GlobalUsages
|
||||||
@@ -46,13 +50,23 @@ 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')";
|
||||||
|
|
||||||
/** 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;
|
private DiviRoles $roles;
|
||||||
|
|
||||||
public function __construct(private ?\wpdb $db = null, ?DiviRoles $roles = null)
|
/** @var array{visible: bool, usable: bool}|null The Theme Builder access of the current report(). */
|
||||||
{
|
private ?array $themeBuilderState = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param (Closure(): array{visible: bool, usable: bool})|null $themeBuilder The caller's Theme Builder access
|
||||||
|
* (DiviIntegration wraps ThemeBuilderAccess – a closure keeps Library free of it): visible = TB objects exist
|
||||||
|
* for the caller, usable = the caller may change them; asked at most once per report(). null = what
|
||||||
|
* ThemeBuilderAccess::canUse() needs, for both (Divi's role option theme_builder, edit_others_posts,
|
||||||
|
* edit_theme_options).
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
private ?\wpdb $db = null,
|
||||||
|
?DiviRoles $roles = null,
|
||||||
|
private ?Closure $themeBuilder = null
|
||||||
|
) {
|
||||||
$this->roles = $roles ?? new DiviRoles();
|
$this->roles = $roles ?? new DiviRoles();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,6 +81,7 @@ class GlobalUsages
|
|||||||
$tooMany = $withBlocked && $live > self::HARD_CAP;
|
$tooMany = $withBlocked && $live > self::HARD_CAP;
|
||||||
$checkEdit = $withBlocked && ! $tooMany;
|
$checkEdit = $withBlocked && ! $tooMany;
|
||||||
|
|
||||||
|
$this->themeBuilderState = null;
|
||||||
$items = [];
|
$items = [];
|
||||||
$hidden = 0;
|
$hidden = 0;
|
||||||
$blocked = [];
|
$blocked = [];
|
||||||
@@ -95,7 +110,7 @@ class GlobalUsages
|
|||||||
_prime_post_caches($ids, false, false);
|
_prime_post_caches($ids, false, false);
|
||||||
foreach ($todo as $row) {
|
foreach ($todo as $row) {
|
||||||
$id = $row['id'];
|
$id = $row['id'];
|
||||||
$readable = current_user_can('read_post', $id);
|
$readable = $this->readable($row);
|
||||||
if ($readable && count($items) < self::LIMIT) {
|
if ($readable && count($items) < self::LIMIT) {
|
||||||
$post = get_post($id);
|
$post = get_post($id);
|
||||||
$items[] = [
|
$items[] = [
|
||||||
@@ -209,21 +224,63 @@ class GlobalUsages
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ruling P8a: whether the caller may edit_post the live post $row; a Theme Builder layout or
|
* Whether $row may be named to the caller: read_post; a Theme Builder post only while the Theme Builder is
|
||||||
* template additionally needs Divi's role option theme_builder (audit L6 – as Divi's own
|
* visible to the caller (review I-2 B: otherwise it reads like a missing ID elsewhere, P12).
|
||||||
* et_pb_is_allowed check for TB layouts, builder/functions.php).
|
*
|
||||||
|
* @param array{id: int, status: string, type: string} $row
|
||||||
|
*/
|
||||||
|
private function readable(array $row): bool
|
||||||
|
{
|
||||||
|
if (self::isThemeBuilder($row) && ! $this->themeBuilder()['visible']) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return current_user_can('read_post', $row['id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ruling P8a: whether the caller may edit_post the live post $row; a Theme Builder post additionally needs the
|
||||||
|
* Theme Builder rights (ruling Q17 M1 – as every Theme Builder gate, ThemeBuilderAccess::canUse(); audit L6).
|
||||||
*
|
*
|
||||||
* @param array{id: int, status: string, type: string} $row
|
* @param array{id: int, status: string, type: string} $row
|
||||||
*/
|
*/
|
||||||
private 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')) {
|
if (self::isThemeBuilder($row) && ! $this->themeBuilder()['usable']) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return current_user_can('edit_post', $row['id']);
|
return current_user_can('edit_post', $row['id']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param array{id: int, status: string, type: string} $row */
|
||||||
|
private static function isThemeBuilder(array $row): bool
|
||||||
|
{
|
||||||
|
return in_array($row['type'], DiviThemeBuilder::postTypes(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array{visible: bool, usable: bool} */
|
||||||
|
private function themeBuilder(): array
|
||||||
|
{
|
||||||
|
if ($this->themeBuilderState !== null) {
|
||||||
|
return $this->themeBuilderState;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->themeBuilder !== null) {
|
||||||
|
$state = ($this->themeBuilder)();
|
||||||
|
} else {
|
||||||
|
$open = $this->roles->allows('theme_builder')
|
||||||
|
&& current_user_can('edit_others_posts')
|
||||||
|
&& current_user_can('edit_theme_options');
|
||||||
|
$state = ['visible' => $open, 'usable' => $open];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->themeBuilderState = [
|
||||||
|
'visible' => (bool) ($state['visible'] ?? false),
|
||||||
|
'usable' => (bool) ($state['usable'] ?? false),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
private function db(): \wpdb
|
private function db(): \wpdb
|
||||||
{
|
{
|
||||||
return $this->db ?? $GLOBALS['wpdb'];
|
return $this->db ?? $GLOBALS['wpdb'];
|
||||||
|
|||||||
@@ -25,7 +25,11 @@ defined('ABSPATH') || exit;
|
|||||||
*/
|
*/
|
||||||
final class LibraryIntegration implements Integration
|
final class LibraryIntegration implements Integration
|
||||||
{
|
{
|
||||||
/** @param (Closure(): bool)|null $diviWritable Whether lit/divi-insert is enabled (audit M2); null = always. */
|
/**
|
||||||
|
* @param (Closure(): bool)|null $diviWritable Whether lit/divi-insert is enabled (audit M2); null = always.
|
||||||
|
* @param GlobalUsages|null $usages The usages with the Theme Builder access (DiviIntegration::globalUsages(),
|
||||||
|
* review I-2); null = GlobalUsages' own fail-safe Theme Builder check.
|
||||||
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private LibraryAccess $access,
|
private LibraryAccess $access,
|
||||||
private DiviPageLoader $loader,
|
private DiviPageLoader $loader,
|
||||||
@@ -35,7 +39,8 @@ final class LibraryIntegration implements Integration
|
|||||||
private ChangeLog $log,
|
private ChangeLog $log,
|
||||||
private DiviModules $modules,
|
private DiviModules $modules,
|
||||||
private ?Closure $diviWritable = null,
|
private ?Closure $diviWritable = null,
|
||||||
private ?ContentSchema $schema = null
|
private ?ContentSchema $schema = null,
|
||||||
|
private ?GlobalUsages $usages = null
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,7 +66,7 @@ final class LibraryIntegration implements Integration
|
|||||||
$this->access,
|
$this->access,
|
||||||
$this->loader,
|
$this->loader,
|
||||||
$this->design,
|
$this->design,
|
||||||
new GlobalUsages(null, $this->access->roles())
|
$this->usages ?? new GlobalUsages(null, $this->access->roles())
|
||||||
),
|
),
|
||||||
new LibrarySaveAbility(
|
new LibrarySaveAbility(
|
||||||
$this->access,
|
$this->access,
|
||||||
|
|||||||
@@ -64,6 +64,17 @@ class DiviThemeBuilder
|
|||||||
/** Actions of wp_insert_post() that name a post's ID after its row exists, earliest first (Q19). */
|
/** Actions of wp_insert_post() that name a post's ID after its row exists, earliest first (Q19). */
|
||||||
private const INSERT_ACTIONS = ['clean_post_cache', 'save_post', 'wp_insert_post'];
|
private const INSERT_ACTIONS = ['clean_post_cache', 'save_post', 'wp_insert_post'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Every Theme Builder post type – the Theme Builder posts, the templates and the layouts (review M-3: one list for
|
||||||
|
* GlobalUsages and lit/library-find-similar; ContentSchema keeps its own literal list in the WordPress layer).
|
||||||
|
*
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
public static function postTypes(): array
|
||||||
|
{
|
||||||
|
return array_merge([self::THEME_BUILDER, self::TEMPLATE], array_values(self::LAYOUT_TYPES));
|
||||||
|
}
|
||||||
|
|
||||||
/** "header", "body" or "footer" for a layout post type, else null. */
|
/** "header", "body" or "footer" for a layout post type, else null. */
|
||||||
public static function areaOf(string $postType): ?string
|
public static function areaOf(string $postType): ?string
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -66,6 +66,20 @@ class ThemeBuilderAccess
|
|||||||
return ($this->visible === null || ($this->visible)()) && $this->canUse() === null;
|
return ($this->visible === null || ($this->visible)()) && $this->canUse() === null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Theme Builder access of a global library edit (GlobalUsages, ruling Q17 M1, review I-2): visible = Theme
|
||||||
|
* Builder posts may be named (visible()); usable = a global edit may change them – the rights of every Theme
|
||||||
|
* Builder write (canUse(), part of visible()) and a writable group, as the Divi tools on layouts.
|
||||||
|
*
|
||||||
|
* @return array{visible: bool, usable: bool}
|
||||||
|
*/
|
||||||
|
public function usageState(): array
|
||||||
|
{
|
||||||
|
$visible = $this->visible();
|
||||||
|
|
||||||
|
return ['visible' => $visible, 'usable' => $visible && $this->writableGate() === null];
|
||||||
|
}
|
||||||
|
|
||||||
public function writableGate(): ?WP_Error
|
public function writableGate(): ?WP_Error
|
||||||
{
|
{
|
||||||
if ($this->writable === null || ($this->writable)()) {
|
if ($this->writable === null || ($this->writable)()) {
|
||||||
|
|||||||
@@ -118,12 +118,15 @@ final class LibraryFixtures
|
|||||||
*
|
*
|
||||||
* @param list<array{id: int, status: string, type: string}>|\Closure(int): list<array{id: int, status: string, type: string}> $rows
|
* @param list<array{id: int, status: string, type: string}>|\Closure(int): list<array{id: int, status: string, type: string}> $rows
|
||||||
*/
|
*/
|
||||||
public static function usages(array|\Closure $rows, ?DiviRoles $roles = null): GlobalUsages
|
public static function usages(
|
||||||
{
|
array|\Closure $rows,
|
||||||
|
?DiviRoles $roles = null,
|
||||||
|
?\Closure $themeBuilder = null
|
||||||
|
): GlobalUsages {
|
||||||
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 ?? self::roles()])->makePartial();
|
$usages = Mockery::mock(GlobalUsages::class, [null, $roles ?? self::roles(), $themeBuilder])->makePartial();
|
||||||
$usages->allows('rows')->andReturnUsing(static fn (int $id): array => is_array($rows) ? $rows : $rows($id));
|
$usages->allows('rows')->andReturnUsing(static fn (int $id): array => is_array($rows) ? $rows : $rows($id));
|
||||||
|
|
||||||
return $usages;
|
return $usages;
|
||||||
|
|||||||
@@ -94,6 +94,38 @@ final class DiviIntegrationTest extends TestCase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Review I-2 (ruling Q17 M1): the library usages follow the Theme Builder access – without it (here: no Divi
|
||||||
|
* role check in unit tests, so canUse() fails) TB layouts are counted and block, never named.
|
||||||
|
* ThemeBuilderAccessTest covers usageState() itself.
|
||||||
|
*/
|
||||||
|
public function testGlobalUsagesFollowTheThemeBuilderAccess(): void
|
||||||
|
{
|
||||||
|
Functions\when('current_user_can')->justReturn(true);
|
||||||
|
Functions\when('get_post')->justReturn(null);
|
||||||
|
Functions\when('_get_non_cached_ids')->returnArg(1);
|
||||||
|
Functions\when('_prime_post_caches')->justReturn(null);
|
||||||
|
Functions\when('wp_cache_delete')->justReturn(true);
|
||||||
|
$db = Mockery::mock(\wpdb::class)->makePartial();
|
||||||
|
$db->allows('get_results')->andReturnUsing(static fn (string $sql): array => str_contains($sql, 'post_content FROM')
|
||||||
|
? [
|
||||||
|
['ID' => '1053', 'post_content' => '<!-- wp:divi/global-layout {"globalModule":"1058"} /-->'],
|
||||||
|
['ID' => '1061', 'post_content' => '<!-- wp:divi/global-layout {"globalModule":"1058"} /-->'],
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
['ID' => '1053', 'post_status' => 'publish', 'post_type' => 'page'],
|
||||||
|
['ID' => '1061', 'post_status' => 'publish', 'post_type' => 'et_header_layout'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$report = (new DiviIntegration(Mockery::mock(ContentSchema::class), FakeChangeLog::forbidden()))
|
||||||
|
->globalUsages($db)
|
||||||
|
->report(1058);
|
||||||
|
|
||||||
|
self::assertSame([1053], array_column($report->items, 'post_id'));
|
||||||
|
self::assertSame([], $report->blocked);
|
||||||
|
self::assertSame(1, $report->blockedHidden);
|
||||||
|
}
|
||||||
|
|
||||||
/** Ruling P9 (E9): lit/divi-node-copy follows lit/divi-insert's Tools-tab state. */
|
/** Ruling P9 (E9): lit/divi-node-copy follows lit/divi-insert's Tools-tab state. */
|
||||||
public function testNodeCopyFollowsLitDiviInsert(): void
|
public function testNodeCopyFollowsLitDiviInsert(): void
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ final class GlobalUsagesTest extends TestCase
|
|||||||
static fn (int $id): WP_Post => new WP_Post(['ID' => $id, 'post_title' => 'Titel ' . $id])
|
static fn (int $id): WP_Post => new WP_Post(['ID' => $id, 'post_title' => 'Titel ' . $id])
|
||||||
);
|
);
|
||||||
Functions\when('current_user_can')->alias(
|
Functions\when('current_user_can')->alias(
|
||||||
static fn (string $cap, int $id): bool => $id !== 1061
|
static fn (string $cap, ?int $id = null): bool => $id !== 1061
|
||||||
);
|
);
|
||||||
|
|
||||||
$report = $usages->report(1058);
|
$report = $usages->report(1058);
|
||||||
@@ -167,24 +167,108 @@ 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. */
|
/** @return list<array{id: int, status: string, type: string}> */
|
||||||
public function testThemeBuilderLayoutsNeedTheThemeBuilderRole(): void
|
private static function themeBuilderRows(): array
|
||||||
{
|
{
|
||||||
$rows = [
|
return [
|
||||||
['id' => 1053, 'status' => 'publish', 'type' => 'page'],
|
['id' => 1053, 'status' => 'publish', 'type' => 'page'],
|
||||||
['id' => 1061, 'status' => 'publish', 'type' => 'et_header_layout'],
|
['id' => 1061, 'status' => 'publish', 'type' => 'et_header_layout'],
|
||||||
['id' => 1062, 'status' => 'publish', 'type' => 'et_body_layout'],
|
['id' => 1062, 'status' => 'publish', 'type' => 'et_body_layout'],
|
||||||
['id' => 1063, 'status' => 'publish', 'type' => 'et_footer_layout'],
|
['id' => 1063, 'status' => 'publish', 'type' => 'et_footer_layout'],
|
||||||
['id' => 1064, 'status' => 'publish', 'type' => 'et_template'],
|
['id' => 1064, 'status' => 'publish', 'type' => 'et_template'],
|
||||||
];
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Audit L6, ruling Q17 M1 (review I-2): without an injected Theme Builder access, Theme Builder rows need what
|
||||||
|
* ThemeBuilderAccess::canUse() needs – Divi's role option theme_builder, edit_others_posts and
|
||||||
|
* edit_theme_options; without them they are neither listed nor named, only counted – and still block.
|
||||||
|
*/
|
||||||
|
public function testThemeBuilderRowsNeedTheThemeBuilderRightsByDefault(): void
|
||||||
|
{
|
||||||
Functions\when('get_post')->justReturn(null);
|
Functions\when('get_post')->justReturn(null);
|
||||||
Functions\when('current_user_can')->justReturn(true);
|
Functions\when('current_user_can')->justReturn(true);
|
||||||
|
|
||||||
$without = LibraryFixtures::usages($rows, LibraryFixtures::roles(['theme_builder']))->report(1058);
|
$with = LibraryFixtures::usages(self::themeBuilderRows(), LibraryFixtures::roles())->report(1058);
|
||||||
$with = LibraryFixtures::usages($rows, LibraryFixtures::roles())->report(1058);
|
|
||||||
|
|
||||||
self::assertSame([1061, 1062, 1063, 1064], $without->blocked);
|
|
||||||
self::assertSame([], $with->blocked);
|
self::assertSame([], $with->blocked);
|
||||||
|
self::assertCount(5, $with->items);
|
||||||
|
|
||||||
|
$without = LibraryFixtures::usages(self::themeBuilderRows(), LibraryFixtures::roles(['theme_builder']))
|
||||||
|
->report(1058);
|
||||||
|
self::assertSame([1053], array_column($without->items, 'post_id'));
|
||||||
|
self::assertSame([], $without->blocked);
|
||||||
|
self::assertSame(4, $without->hidden);
|
||||||
|
self::assertSame(4, $without->blockedHidden);
|
||||||
|
self::assertTrue($without->blocks());
|
||||||
|
|
||||||
|
Functions\when('current_user_can')->alias(static fn (string $cap): bool => $cap !== 'edit_theme_options');
|
||||||
|
$noThemeOptions = LibraryFixtures::usages(self::themeBuilderRows(), LibraryFixtures::roles())->report(1058);
|
||||||
|
self::assertSame(4, $noThemeOptions->blockedHidden);
|
||||||
|
self::assertSame([], $noThemeOptions->blocked);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Ruling Q17 M1: a Theme Builder row the account may see but not use (no edit right in the TB) blocks by ID. */
|
||||||
|
public function testAVisibleButUnusableThemeBuilderRowBlocksByItsId(): void
|
||||||
|
{
|
||||||
|
Functions\when('get_post')->justReturn(null);
|
||||||
|
Functions\when('current_user_can')->justReturn(true);
|
||||||
|
$asked = 0;
|
||||||
|
$themeBuilder = static function () use (&$asked): array {
|
||||||
|
++$asked;
|
||||||
|
|
||||||
|
return ['visible' => true, 'usable' => false];
|
||||||
|
};
|
||||||
|
|
||||||
|
$report = LibraryFixtures::usages(self::themeBuilderRows(), null, $themeBuilder)->report(1058);
|
||||||
|
|
||||||
|
self::assertCount(5, $report->items);
|
||||||
|
self::assertSame([1061, 1062, 1063, 1064], $report->blocked);
|
||||||
|
self::assertSame(1, $asked, 'evaluated once per report');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Review I-2 scenario B: without Theme Builder visibility a TB row is only counted – never its ID, title or type. */
|
||||||
|
public function testWithoutThemeBuilderVisibilityRowsAreOnlyCounted(): void
|
||||||
|
{
|
||||||
|
Functions\when('get_post')->alias(
|
||||||
|
static fn (int $id): WP_Post => new WP_Post(['ID' => $id, 'post_title' => 'Titel ' . $id])
|
||||||
|
);
|
||||||
|
Functions\when('current_user_can')->justReturn(true);
|
||||||
|
|
||||||
|
$report = LibraryFixtures::usages(
|
||||||
|
self::themeBuilderRows(),
|
||||||
|
null,
|
||||||
|
static fn (): array => ['visible' => false, 'usable' => false]
|
||||||
|
)->report(1058);
|
||||||
|
|
||||||
|
self::assertSame(
|
||||||
|
[['post_id' => 1053, 'title' => 'Titel 1053', 'status' => 'publish', 'type' => 'page']],
|
||||||
|
$report->items
|
||||||
|
);
|
||||||
|
self::assertSame(4, $report->hidden);
|
||||||
|
self::assertSame([], $report->blocked);
|
||||||
|
self::assertSame(4, $report->blockedHidden);
|
||||||
|
$json = (string) json_encode($report->details());
|
||||||
|
self::assertStringNotContainsString('1061', $json);
|
||||||
|
self::assertStringNotContainsString('et_header_layout', $json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A page row is unaffected by the Theme Builder access. */
|
||||||
|
public function testPageRowsIgnoreTheThemeBuilderAccess(): void
|
||||||
|
{
|
||||||
|
Functions\when('get_post')->justReturn(null);
|
||||||
|
Functions\when('current_user_can')->alias(static fn (string $cap, mixed $id = null): bool => $cap !== 'edit_post');
|
||||||
|
$asked = 0;
|
||||||
|
$themeBuilder = static function () use (&$asked): array {
|
||||||
|
++$asked;
|
||||||
|
|
||||||
|
return ['visible' => false, 'usable' => false];
|
||||||
|
};
|
||||||
|
|
||||||
|
$report = LibraryFixtures::usages(LibraryFixtures::rows([1053]), null, $themeBuilder)->report(1058);
|
||||||
|
|
||||||
|
self::assertSame([1053], array_column($report->items, 'post_id'));
|
||||||
|
self::assertSame([1053], $report->blocked);
|
||||||
|
self::assertSame(0, $asked, 'no Theme Builder row, no Theme Builder check');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testAListingSkipsTheEditChecks(): void
|
public function testAListingSkipsTheEditChecks(): void
|
||||||
|
|||||||
@@ -33,6 +33,25 @@ final class ThemeBuilderAccessTest extends TestCase
|
|||||||
self::assertToolError(ThemeBuilderFixtures::access()->canUse(), 'forbidden');
|
self::assertToolError(ThemeBuilderFixtures::access()->canUse(), 'forbidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Ruling Q17 M1 (review I-2): what a global library edit may do with Theme Builder posts (GlobalUsages). */
|
||||||
|
public function testUsageStateOfAGlobalEdit(): void
|
||||||
|
{
|
||||||
|
LibraryFixtures::given([]);
|
||||||
|
self::assertSame(['visible' => true, 'usable' => true], ThemeBuilderFixtures::access()->usageState());
|
||||||
|
self::assertSame(['visible' => true, 'usable' => false], ThemeBuilderFixtures::access([], false)->usageState());
|
||||||
|
self::assertSame(
|
||||||
|
['visible' => false, 'usable' => false],
|
||||||
|
ThemeBuilderFixtures::access([], true, null, false)->usageState()
|
||||||
|
);
|
||||||
|
self::assertSame(
|
||||||
|
['visible' => false, 'usable' => false],
|
||||||
|
ThemeBuilderFixtures::access(['theme_builder'])->usageState()
|
||||||
|
);
|
||||||
|
|
||||||
|
LibraryFixtures::given([], ['edit_theme_options']);
|
||||||
|
self::assertSame(['visible' => false, 'usable' => false], ThemeBuilderFixtures::access()->usageState());
|
||||||
|
}
|
||||||
|
|
||||||
/** Ruling Q15: Divi's Theme Builder page also needs edit_theme_options (theme-builder.php:2059-2063). */
|
/** Ruling Q15: Divi's Theme Builder page also needs edit_theme_options (theme-builder.php:2059-2063). */
|
||||||
public function testWithoutEditThemeOptionsTheThemeBuilderIsHidden(): void
|
public function testWithoutEditThemeOptionsTheThemeBuilderIsHidden(): void
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user