feat: Theme Builder adapter, template value and per-object access
DiviThemeBuilder wraps Divi's Theme Builder: reads through Divi's functions, writes layouts/templates through insert_layout/store_template behind a wp_die guard (edit_others_posts and function checks first, every handler filter and output buffer restored in finally), list/conditions/enabled mirrored from Divi's own save. Template: live = attached and enabled, whatever the post status (ruling Q8/B8). ThemeBuilderAccess: edit_others_posts plus Divi's theme_builder role, hidden IDs answer like missing ones, canEditTemplate (B11) and layoutsRefusal (B10). DiviCompatibility checks the Theme Builder functions and post types (E15). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Tests\Support;
|
||||
|
||||
use LIT\MCP\Integrations\Divi\ThemeBuilder\DiviThemeBuilder;
|
||||
use LIT\MCP\Integrations\Divi\ThemeBuilder\Template;
|
||||
use LIT\MCP\Integrations\Divi\ThemeBuilder\ThemeBuilderAccess;
|
||||
use Mockery;
|
||||
use WP_Post;
|
||||
|
||||
/**
|
||||
* The Theme Builder of the tests: the real builder posts of tests/Fixtures/divi/stage3 (live TB post 1064,
|
||||
* default template 1065, template 1066 with header 1061, body 1062, footer 1063 – both disabled), a store
|
||||
* answering from Template values and the access built from them. Write methods of the store are real
|
||||
* unless a test mocks them.
|
||||
*/
|
||||
final class ThemeBuilderFixtures
|
||||
{
|
||||
public const THEME_BUILDER = 1064;
|
||||
public const DEFAULT_TEMPLATE = 1065;
|
||||
public const TEMPLATE = 1066;
|
||||
public const HEADER = 1061;
|
||||
public const BODY = 1062;
|
||||
public const FOOTER = 1063;
|
||||
|
||||
/** @var array<int, array{string, string}> Layout ID => [stage3 fixture file, post type]. */
|
||||
public const LAYOUTS = [
|
||||
1061 => ['tb-header--globaler-header.html', 'et_header_layout'],
|
||||
1062 => ['tb-body--body-mit-dynamic-content.html', 'et_body_layout'],
|
||||
1063 => ['tb-footer--footer.html', 'et_footer_layout'],
|
||||
];
|
||||
|
||||
/** @param array<string, mixed> $fields */
|
||||
public static function layout(int $id, array $fields = []): WP_Post
|
||||
{
|
||||
return new WP_Post(array_merge([
|
||||
'ID' => $id,
|
||||
'post_type' => self::LAYOUTS[$id][1],
|
||||
'post_status' => 'publish',
|
||||
'post_title' => '[Fixture] ' . $id,
|
||||
'post_content' => DiviFixtures::markup('stage3/' . self::LAYOUTS[$id][0]),
|
||||
], $fields));
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $fields */
|
||||
public static function templatePost(int $id = self::TEMPLATE, array $fields = []): WP_Post
|
||||
{
|
||||
return new WP_Post(array_merge([
|
||||
'ID' => $id,
|
||||
'post_type' => DiviThemeBuilder::TEMPLATE,
|
||||
'post_status' => 'publish',
|
||||
'post_title' => $id === self::DEFAULT_TEMPLATE ? 'Standard-Website-Template' : '[Fixture] Vorlage Globale Instanzen',
|
||||
'post_content' => '',
|
||||
], $fields));
|
||||
}
|
||||
|
||||
/**
|
||||
* The template as Divi reads it (et_theme_builder_get_template); $overrides replace keys of that
|
||||
* array and may set status, attached and marked_unused.
|
||||
*
|
||||
* @param array<string, mixed> $overrides
|
||||
*/
|
||||
public static function template(int $id = self::TEMPLATE, array $overrides = []): Template
|
||||
{
|
||||
$base = $id === self::DEFAULT_TEMPLATE
|
||||
? [
|
||||
'id' => $id,
|
||||
'title' => 'Standard-Website-Template',
|
||||
'default' => true,
|
||||
'enabled' => false,
|
||||
'layouts' => [
|
||||
'header' => ['id' => self::HEADER, 'enabled' => true],
|
||||
'body' => ['id' => 0, 'enabled' => true],
|
||||
'footer' => ['id' => 0, 'enabled' => true],
|
||||
],
|
||||
'use_on' => [],
|
||||
'exclude_from' => [],
|
||||
]
|
||||
: [
|
||||
'id' => $id,
|
||||
'title' => '[Fixture] Vorlage Globale Instanzen',
|
||||
'default' => false,
|
||||
'enabled' => false,
|
||||
'layouts' => [
|
||||
'header' => ['id' => self::HEADER, 'enabled' => true],
|
||||
'body' => ['id' => self::BODY, 'enabled' => true],
|
||||
'footer' => ['id' => self::FOOTER, 'enabled' => true],
|
||||
],
|
||||
'use_on' => ['singular:post_type:page:id:1053'],
|
||||
'exclude_from' => [],
|
||||
];
|
||||
|
||||
return Template::fromDivi(
|
||||
array_merge($base, array_intersect_key($overrides, $base)),
|
||||
(string) ($overrides['status'] ?? 'publish'),
|
||||
(bool) ($overrides['attached'] ?? true),
|
||||
isset($overrides['marked_unused']) ? (string) $overrides['marked_unused'] : null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A store whose live list is $templates, with a TB draft when $draftId > 0, $detached as templates no
|
||||
* Theme Builder post lists and $byIdOnly as templates template() finds but no list names (e.g. the one a
|
||||
* test lets storeTemplate() create).
|
||||
*
|
||||
* @param list<Template> $templates
|
||||
* @param list<Template> $detached
|
||||
* @param list<Template> $byIdOnly
|
||||
*/
|
||||
public static function store(
|
||||
array $templates = [],
|
||||
int $draftId = 0,
|
||||
array $detached = [],
|
||||
array $byIdOnly = []
|
||||
): DiviThemeBuilder {
|
||||
$byId = [];
|
||||
foreach (array_merge($templates, $detached, $byIdOnly) as $template) {
|
||||
$byId[$template->id] = $template;
|
||||
}
|
||||
$store = Mockery::mock(DiviThemeBuilder::class)->makePartial();
|
||||
$store->allows('liveId')->andReturn(self::THEME_BUILDER);
|
||||
$store->allows('draftId')->andReturn($draftId);
|
||||
$store->allows('liveTemplates')->andReturn($templates);
|
||||
$store->allows('templateIds')->andReturn(array_map(static fn (Template $t): int => $t->id, $templates));
|
||||
$store->allows('template')->andReturnUsing(static fn (int $id): ?Template => $byId[$id] ?? null);
|
||||
$store->allows('detachedIds')->andReturn(array_map(static fn (Template $t): int => $t->id, $detached));
|
||||
$store->allows('flatConditions')->andReturn(self::conditions());
|
||||
|
||||
return $store;
|
||||
}
|
||||
|
||||
/** @param list<string> $deniedRoles Divi role options the user lacks. */
|
||||
public static function access(
|
||||
array $deniedRoles = [],
|
||||
bool $writable = true,
|
||||
?DiviThemeBuilder $store = null,
|
||||
bool $visible = true
|
||||
): ThemeBuilderAccess {
|
||||
return new ThemeBuilderAccess(
|
||||
$store ?? self::store(),
|
||||
LibraryFixtures::roles($deniedRoles),
|
||||
static fn (): bool => $writable,
|
||||
static fn (): bool => $visible
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* An excerpt of et_theme_builder_get_flat_template_settings_options() as the adapter returns it
|
||||
* (live DDEV dump 2026-09-27 plus the Pods type lit_person).
|
||||
*
|
||||
* @return array<string, array{label: string, type: ?string, value: ?string}>
|
||||
*/
|
||||
public static function conditions(): array
|
||||
{
|
||||
return [
|
||||
'singular:post_type:page:all' => ['label' => 'Alle Seiten', 'type' => null, 'value' => null],
|
||||
'homepage' => ['label' => 'Startseite', 'type' => null, 'value' => null],
|
||||
'singular:post_type:page:id:' => ['label' => 'Bestimmte Seiten', 'type' => 'post_type', 'value' => 'page'],
|
||||
'singular:post_type:page:children:id:' => [
|
||||
'label' => 'Unterseiten bestimmter Seiten', 'type' => 'post_type', 'value' => 'page',
|
||||
],
|
||||
'singular:post_type:lit_person:all' => ['label' => 'Alle Personen', 'type' => null, 'value' => null],
|
||||
'singular:post_type:lit_person:id:' => [
|
||||
'label' => 'Bestimmte Personen', 'type' => 'post_type', 'value' => 'lit_person',
|
||||
],
|
||||
'archive:taxonomy:category:term:id:' => [
|
||||
'label' => 'Bestimmte Kategorien', 'type' => 'taxonomy', 'value' => 'category',
|
||||
],
|
||||
'archive:user:id:' => ['label' => 'Bestimmte Autoren', 'type' => 'user', 'value' => null],
|
||||
'archive:user:role:' => ['label' => 'Autoren mit Rolle', 'type' => 'user_role', 'value' => null],
|
||||
'search' => ['label' => 'Suchergebnisse', 'type' => null, 'value' => null],
|
||||
'404' => ['label' => '404-Seite', 'type' => null, 'value' => null],
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user