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:
@@ -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. */
|
||||
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])
|
||||
);
|
||||
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);
|
||||
@@ -167,24 +167,108 @@ 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
|
||||
/** @return list<array{id: int, status: string, type: string}> */
|
||||
private static function themeBuilderRows(): array
|
||||
{
|
||||
$rows = [
|
||||
return [
|
||||
['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'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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('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);
|
||||
$with = LibraryFixtures::usages(self::themeBuilderRows(), LibraryFixtures::roles())->report(1058);
|
||||
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
|
||||
|
||||
@@ -33,6 +33,25 @@ final class ThemeBuilderAccessTest extends TestCase
|
||||
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). */
|
||||
public function testWithoutEditThemeOptionsTheThemeBuilderIsHidden(): void
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user