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,240 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\ThemeBuilder;
|
||||
|
||||
use Closure;
|
||||
use LIT\MCP\Abilities\Errors;
|
||||
use LIT\MCP\Integrations\Divi\Library\DiviRoles;
|
||||
use LIT\MCP\Integrations\WordPress\PostGuard;
|
||||
use WP_Error;
|
||||
use WP_Post;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* Rights on Theme Builder objects (spec §4.5, E3 – never laxer than Divi): the Theme Builder at all needs
|
||||
* edit_others_posts (Divi's own TB requirement, F19/F23) and the Divi role option theme_builder (off for
|
||||
* every role but administrator by default, F12); one layout or template additionally edit_post on it.
|
||||
* Without those, or with the group "Divi Theme Builder" disabled, every TB ID answers like a missing ID
|
||||
* (P12 pattern). The group's "nur lesen" stops writes (E9). Not final so abilities can be tested with a mock.
|
||||
*/
|
||||
class ThemeBuilderAccess
|
||||
{
|
||||
/** Tool group "Divi Theme Builder" (Tools tab). */
|
||||
public const GROUP = 'divi-theme-builder';
|
||||
|
||||
/**
|
||||
* @param (Closure(): bool)|null $writable Group writable (neither "nur lesen" nor disabled); null = always.
|
||||
* @param (Closure(): bool)|null $visible Group not disabled; null = always.
|
||||
*/
|
||||
public function __construct(
|
||||
private DiviThemeBuilder $store,
|
||||
private DiviRoles $roles,
|
||||
private ?Closure $writable = null,
|
||||
private ?Closure $visible = null
|
||||
) {
|
||||
}
|
||||
|
||||
public function store(): DiviThemeBuilder
|
||||
{
|
||||
return $this->store;
|
||||
}
|
||||
|
||||
public function roles(): DiviRoles
|
||||
{
|
||||
return $this->roles;
|
||||
}
|
||||
|
||||
public function canUse(): ?WP_Error
|
||||
{
|
||||
if (! current_user_can('edit_others_posts')) {
|
||||
return Errors::forbidden('Keine Berechtigung für den Divi Theme Builder (edit_others_posts).');
|
||||
}
|
||||
|
||||
return $this->roles->refusal('theme_builder');
|
||||
}
|
||||
|
||||
/** Whether Theme Builder objects exist for this connection: group not disabled and canUse(). */
|
||||
public function visible(): bool
|
||||
{
|
||||
return ($this->visible === null || ($this->visible)()) && $this->canUse() === null;
|
||||
}
|
||||
|
||||
public function writableGate(): ?WP_Error
|
||||
{
|
||||
if ($this->writable === null || ($this->writable)()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Errors::forbidden(
|
||||
'Schreiben im Divi Theme Builder ist für diese Verbindung abgeschaltet (Gruppe „Divi Theme Builder“ auf '
|
||||
. '„nur lesen“ oder deaktiviert).',
|
||||
'Einen Administrator fragen: Einstellungen → LIT MCP → Tools.'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Layout $postId for the Divi tools (E1): null when it is no Theme Builder layout (the caller goes on);
|
||||
* like a missing ID without visible(); forbidden without edit_post; for writing also the group gate and
|
||||
* a published layout (Divi's TB layouts are always published).
|
||||
*/
|
||||
public function layout(mixed $postId, bool $forWrite): WP_Post|WP_Error|null
|
||||
{
|
||||
$id = PostGuard::id($postId);
|
||||
$post = $id === null ? null : get_post($id);
|
||||
if (! $post instanceof WP_Post || DiviThemeBuilder::areaOf((string) $post->post_type) === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$id = (int) $post->ID;
|
||||
if (! $this->visible()) {
|
||||
return PostGuard::notFound($id);
|
||||
}
|
||||
|
||||
if (! current_user_can('edit_post', $id)) {
|
||||
return Errors::forbidden(sprintf('Keine Berechtigung, Theme-Builder-Layout %d zu bearbeiten.', $id));
|
||||
}
|
||||
|
||||
if (! $forWrite) {
|
||||
return $post;
|
||||
}
|
||||
|
||||
$gate = $this->writableGate();
|
||||
if ($gate !== null) {
|
||||
return $gate;
|
||||
}
|
||||
|
||||
if ((string) $post->post_status !== 'publish') {
|
||||
return Errors::invalidInput(
|
||||
sprintf(
|
||||
'Theme-Builder-Layout %d ist nicht aktiv (Status „%s“); nichts wurde gespeichert.',
|
||||
$id,
|
||||
(string) $post->post_status
|
||||
),
|
||||
['post_id' => 'nur aktive Theme-Builder-Layouts']
|
||||
);
|
||||
}
|
||||
|
||||
return $post;
|
||||
}
|
||||
|
||||
/** Template $templateId for the Theme Builder tools; any other ID, or a hidden Theme Builder, is not found. */
|
||||
public function template(mixed $templateId, bool $forWrite): WP_Post|WP_Error
|
||||
{
|
||||
$id = PostGuard::id($templateId);
|
||||
if ($id === null) {
|
||||
return Errors::invalidInput(
|
||||
'template_id fehlt oder ist keine ID.',
|
||||
['template_id' => 'ID aus lit/tb-list']
|
||||
);
|
||||
}
|
||||
|
||||
$post = get_post($id);
|
||||
if (
|
||||
! $post instanceof WP_Post
|
||||
|| $post->post_type !== DiviThemeBuilder::TEMPLATE
|
||||
|| (string) $post->post_status !== 'publish'
|
||||
|| ! $this->visible()
|
||||
) {
|
||||
return Errors::notFound(
|
||||
sprintf('Theme-Builder-Vorlage %d gibt es nicht.', $id),
|
||||
'Vorlagen mit lit/tb-list auflisten.'
|
||||
);
|
||||
}
|
||||
|
||||
if (! current_user_can('edit_post', $id)) {
|
||||
return Errors::forbidden(sprintf('Keine Berechtigung, Theme-Builder-Vorlage %d zu bearbeiten.', $id));
|
||||
}
|
||||
|
||||
return $forWrite ? ($this->writableGate() ?? $post) : $post;
|
||||
}
|
||||
|
||||
/** Whether template $id may be named to this user (tb-list shows other IDs not at all, ruling Q8/B11). */
|
||||
public function canEditTemplate(int $id): bool
|
||||
{
|
||||
return $this->visible() && current_user_can('edit_post', $id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Refusal when a layout $template refers to is no longer a published layout of its area (Divi trashes
|
||||
* layouts marked unused after seven days, stage3b README finding 10); reattach and activation need this
|
||||
* first (ruling Q8/B10). Areas without a layout (ID 0) need nothing.
|
||||
*/
|
||||
public function layoutsRefusal(Template $template): ?WP_Error
|
||||
{
|
||||
$fields = [];
|
||||
foreach ($template->layouts as $area => $layout) {
|
||||
if ($layout['id'] <= 0) {
|
||||
continue;
|
||||
}
|
||||
$post = get_post($layout['id']);
|
||||
if (
|
||||
! $post instanceof WP_Post
|
||||
|| $post->post_type !== (DiviThemeBuilder::LAYOUT_TYPES[$area] ?? null)
|
||||
|| (string) $post->post_status !== 'publish'
|
||||
) {
|
||||
$fields['layouts.' . $area] = sprintf(
|
||||
'Layout %d ist kein aktives Theme-Builder-Layout (%s) mehr',
|
||||
$layout['id'],
|
||||
$area
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ($fields === []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Errors::invalidInput(
|
||||
sprintf(
|
||||
'Theme-Builder-Vorlage %d verweist auf Layouts, die nicht mehr aktiv sind; nichts wurde gespeichert.',
|
||||
$template->id
|
||||
),
|
||||
$fields,
|
||||
'Im Theme Builder (Divi → Theme Builder) die Layouts der Vorlage wiederherstellen oder ersetzen, '
|
||||
. 'dann erneut aufrufen.'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creating a template with layouts: canUse(), the group gate and publish_posts of the template and
|
||||
* layout types – Divi publishes them at once (F17, F18).
|
||||
*/
|
||||
public function canCreate(): ?WP_Error
|
||||
{
|
||||
$refusal = $this->canUse() ?? $this->writableGate();
|
||||
if ($refusal !== null) {
|
||||
return $refusal;
|
||||
}
|
||||
|
||||
foreach (array_merge([DiviThemeBuilder::TEMPLATE], array_values(DiviThemeBuilder::LAYOUT_TYPES)) as $type) {
|
||||
$object = get_post_type_object($type);
|
||||
$cap = is_object($object) && isset($object->cap->publish_posts)
|
||||
? (string) $object->cap->publish_posts
|
||||
: 'publish_posts';
|
||||
if (! current_user_can($cap)) {
|
||||
return Errors::forbidden(
|
||||
sprintf('Keine Berechtigung, Inhalte des Theme Builders zu veröffentlichen (%s).', $cap)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/** E7: while the Theme Builder has unsaved changes (draft), its later save would replace the template list. */
|
||||
public function draftRefusal(): ?WP_Error
|
||||
{
|
||||
if ($this->store->draftId() <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Errors::unsupported(
|
||||
'Der Divi Theme Builder hat ungespeicherte Änderungen (Entwurf); solange ändert LIT MCP keine Vorlagen – '
|
||||
. 'ein späteres Speichern dort würde die Zuordnung ersetzen. Nichts wurde gespeichert.',
|
||||
'Im Theme Builder (Divi → Theme Builder) speichern oder die Änderungen verwerfen, dann erneut aufrufen.'
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user