feat: lit/tb-assign – conditions with dry run on live templates, reattach

- ThemeBuilderAccess::changeableTemplate(): rights (Q18 not_found), group gate,
  no TB draft, never the default template, current template_rev
- omitted exclude_from stays, [] clears (Q3); live dry run shows use_on and
  exclude_from before/after, overlaps and possible overlaps (B9)
- reattach only with published layouts (B10) and a live TB post; always
  disabled, unmarks template and layouts; failure restores everything

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-27 16:28:27 +02:00
co-authored by Claude Opus 5.5
parent 2827fd4bd8
commit 0975b2f69b
8 changed files with 837 additions and 1 deletions
@@ -154,6 +154,23 @@ class ThemeBuilderAccess
return $forWrite ? ($this->writableGate() ?? $post) : $post;
}
/**
* A template the Theme Builder tools may change (lit/tb-assign, lit/tb-activate): rights and group gate
* (template(), a template the user may not edit reads as missing – ruling Q18), no Theme Builder draft (E7),
* never the default template (E8), the current template_rev.
*
* @param array<string, mixed> $input template_id, template_rev
*/
public function changeableTemplate(array $input): Template|WP_Error
{
$post = $this->template($input['template_id'] ?? null, true);
if ($post instanceof WP_Error) {
return $post;
}
return $this->checkedTemplate((int) $post->ID, $input);
}
/**
* Whether template $id may be named to this user (tb-list shows other IDs not at all, ruling Q8/B11).
* $id must come from Divi's own template list (templateIds()/liveTemplates()/detachedIds()): this checks
@@ -244,4 +261,42 @@ class ThemeBuilderAccess
'Im Theme Builder (Divi → Theme Builder) speichern oder die Änderungen verwerfen, dann erneut aufrufen.'
);
}
/**
* The draft, default and template_rev checks of changeableTemplate() for template $id (rights already checked).
*
* @param array<string, mixed> $input
*/
private function checkedTemplate(int $id, array $input): Template|WP_Error
{
$refusal = $this->draftRefusal();
if ($refusal !== null) {
return $refusal;
}
$attached = in_array($id, $this->store->templateIds($this->store->liveId()), true);
$template = $this->store->template($id, $attached);
if ($template === null) {
return Errors::notFound(
sprintf('Theme-Builder-Vorlage %d gibt es nicht.', $id),
'Vorlagen mit lit/tb-list auflisten.'
);
}
if ($template->default) {
return Errors::unsupported(
'Die Standardvorlage ändert LIT MCP nicht; nichts wurde gespeichert.',
'Die Standardvorlage im Divi Theme Builder bearbeiten.'
);
}
if (! is_string($input['template_rev'] ?? null) || $input['template_rev'] !== $template->rev()) {
return Errors::revMismatch(
'Die Vorlage wurde seit dem Lesen geändert (template_rev passt nicht); nichts wurde gespeichert.',
'Mit lit/tb-list neu lesen und mit der neuen template_rev wiederholen.'
);
}
return $template;
}
}