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,153 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\ThemeBuilder;
|
||||
|
||||
use LIT\MCP\Abilities\Rev;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* A Theme Builder template as Divi reads it (et_theme_builder_get_template(), theme-builder.php:471-521;
|
||||
* fixtures stage3/stage3b), plus whether the live Theme Builder post lists it (attached) and Divi's
|
||||
* "unused" mark (set when a Theme Builder save drops it from the list, stage3b README finding 10).
|
||||
* rev() is template_rev, the lock of the Theme Builder tools. No WordPress calls.
|
||||
*/
|
||||
final class Template
|
||||
{
|
||||
public const AREAS = ['header', 'body', 'footer'];
|
||||
|
||||
/**
|
||||
* @param array<string, array{id: int, enabled: bool}> $layouts
|
||||
* @param list<string> $useOn
|
||||
* @param list<string> $excludeFrom
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly int $id,
|
||||
public readonly string $title,
|
||||
public readonly string $status,
|
||||
public readonly bool $default,
|
||||
public readonly bool $enabled,
|
||||
public readonly array $layouts,
|
||||
public readonly array $useOn,
|
||||
public readonly array $excludeFrom,
|
||||
public readonly bool $attached,
|
||||
public readonly ?string $markedUnused
|
||||
) {
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $divi et_theme_builder_get_template() */
|
||||
public static function fromDivi(array $divi, string $status, bool $attached, ?string $markedUnused): self
|
||||
{
|
||||
$layouts = [];
|
||||
foreach (self::AREAS as $area) {
|
||||
$layout = is_array($divi['layouts'][$area] ?? null) ? $divi['layouts'][$area] : [];
|
||||
$layouts[$area] = ['id' => (int) ($layout['id'] ?? 0), 'enabled' => (bool) ($layout['enabled'] ?? true)];
|
||||
}
|
||||
|
||||
return new self(
|
||||
(int) ($divi['id'] ?? 0),
|
||||
(string) ($divi['title'] ?? ''),
|
||||
$status,
|
||||
(bool) ($divi['default'] ?? false),
|
||||
(bool) ($divi['enabled'] ?? false),
|
||||
$layouts,
|
||||
self::strings($divi['use_on'] ?? []),
|
||||
self::strings($divi['exclude_from'] ?? []),
|
||||
$attached,
|
||||
$markedUnused
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether visitors can see it: listed by the live Theme Builder post and enabled. Divi renders such a
|
||||
* template whatever its post status (et_theme_builder_get_template() and ThemeBuilderRequest only read
|
||||
* the enabled flag), so the status does not count here – fail closed (ruling Q8/B8).
|
||||
*/
|
||||
public function isLive(): bool
|
||||
{
|
||||
return $this->attached && $this->enabled;
|
||||
}
|
||||
|
||||
/** Whether visitors see layout $layoutId as its $area through this template. */
|
||||
public function showsLayout(string $area, int $layoutId): bool
|
||||
{
|
||||
return $this->isLive()
|
||||
&& ($this->layouts[$area]['id'] ?? 0) === $layoutId
|
||||
&& ($this->layouts[$area]['enabled'] ?? false);
|
||||
}
|
||||
|
||||
/** template_rev: changes with everything the Theme Builder tools read or write. */
|
||||
public function rev(): string
|
||||
{
|
||||
$state = [$this->id, $this->title, $this->enabled, $this->layouts];
|
||||
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode -- pure value object, no WordPress.
|
||||
return Rev::of((string) json_encode(
|
||||
array_merge($state, [$this->useOn, $this->excludeFrom, $this->attached]),
|
||||
JSON_UNESCAPED_UNICODE
|
||||
));
|
||||
}
|
||||
|
||||
/** @return list<string> Post types of its singular conditions (singular:post_type:<type>:…). */
|
||||
public function postTypes(): array
|
||||
{
|
||||
$types = [];
|
||||
foreach ($this->useOn as $condition) {
|
||||
$parts = explode(':', $condition);
|
||||
if (($parts[0] ?? '') === 'singular' && ($parts[1] ?? '') === 'post_type' && ($parts[2] ?? '') !== '') {
|
||||
$types[] = $parts[2];
|
||||
}
|
||||
}
|
||||
|
||||
return array_values(array_unique($types));
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'title' => $this->title,
|
||||
'default' => $this->default,
|
||||
'enabled' => $this->enabled,
|
||||
'live' => $this->isLive(),
|
||||
'attached' => $this->attached,
|
||||
'marked_unused' => $this->markedUnused,
|
||||
'template_rev' => $this->rev(),
|
||||
'layouts' => $this->layouts,
|
||||
'use_on' => $this->useOn,
|
||||
'exclude_from' => $this->excludeFrom,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Live templates other than $exceptId that share a condition of $useOn (Divi then picks one of them).
|
||||
*
|
||||
* @param list<Template> $live
|
||||
* @param list<string> $useOn
|
||||
* @return list<array{id: int, title: string, conditions: list<string>}>
|
||||
*/
|
||||
public static function overlaps(array $live, int $exceptId, array $useOn): array
|
||||
{
|
||||
$overlaps = [];
|
||||
foreach ($live as $other) {
|
||||
if ($other->id === $exceptId || ! $other->isLive()) {
|
||||
continue;
|
||||
}
|
||||
$shared = array_values(array_intersect($other->useOn, $useOn));
|
||||
if ($shared !== []) {
|
||||
$overlaps[] = ['id' => $other->id, 'title' => $other->title, 'conditions' => $shared];
|
||||
}
|
||||
}
|
||||
|
||||
return $overlaps;
|
||||
}
|
||||
|
||||
/** @return list<string> */
|
||||
private static function strings(mixed $list): array
|
||||
{
|
||||
return array_values(array_map('strval', array_filter(is_array($list) ? $list : [], 'is_scalar')));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user