'array', 'maxItems' => TemplateConditions::MAX, 'items' => ['type' => 'string']]; return [ 'type' => 'object', 'properties' => [ 'template_id' => ['type' => 'integer', 'minimum' => 1], 'template_rev' => ['type' => 'string', 'description' => 'template_rev aus lit/tb-list.'], 'use_on' => $conditions, 'exclude_from' => $conditions + [ 'description' => 'Fehlt es, bleiben die bisherigen Ausnahmen; [] entfernt sie.', ], 'reattach' => ['type' => 'boolean'], 'confirm' => ['type' => 'boolean'], ], 'required' => ['template_id', 'template_rev', 'use_on'], 'additionalProperties' => false, ]; } public function outputSchema(): ?array { return ToolResult::schema([ 'template_id' => ['type' => 'integer'], 'template_rev' => ['type' => 'string'], 'live' => ['type' => 'boolean'], 'previous' => ['type' => 'object'], ], ['template_id', 'template_rev', 'live']); } public function annotations(): array { return Annotations::destructive(true); } public function isDirect(): bool { return false; } public function permission(array $input): bool|WP_Error { $template = $this->access->changeableTemplate($input); return $template instanceof WP_Error ? $template : true; } /** @throws WriteFailed */ public function execute(array $input): mixed { $template = $this->access->changeableTemplate($input); if ($template instanceof WP_Error) { return $template; } $useOn = is_array($input['use_on'] ?? null) ? $this->conditions->check($input['use_on'], 'use_on') : ['ids' => [], 'errors' => ['use_on' => 'Liste von Bedingungs-IDs erwartet (leere Liste = keine)']]; $excludeFrom = match (true) { ! array_key_exists('exclude_from', $input) => ['ids' => $template->excludeFrom, 'errors' => []], is_array($input['exclude_from']) => $this->conditions->check($input['exclude_from'], 'exclude_from'), default => [ 'ids' => [], 'errors' => ['exclude_from' => 'Liste von Bedingungs-IDs erwartet ([] entfernt die Ausnahmen)'], ], }; $errors = array_merge($useOn['errors'], $excludeFrom['errors']); if ($errors !== []) { return Errors::invalidInput('Bedingungen sind ungültig; nichts wurde gespeichert.', $errors); } $reattach = ! $template->attached && ($input['reattach'] ?? false) === true; $refusal = $this->reattachRefusal($template, $reattach); if ($refusal !== null) { return $refusal; } if ($template->isLive() && ($input['confirm'] ?? false) !== true) { return $this->dryRun($template, $useOn['ids'], $excludeFrom['ids']); } return $this->write($template, $useOn['ids'], $excludeFrom['ids'], $reattach); } /** * A detached template needs reattach: true, a live Theme Builder post and published layouts (B10). Detached * means what tb-list shows as detached: no Theme Builder post lists it – a template of a Theme Builder library * item or draft is none (audit L4). */ private function reattachRefusal(Template $template, bool $reattach): ?WP_Error { if ($template->attached) { return null; } if ($this->access->store()->isListedAnywhere($template->id)) { return Errors::invalidInput( sprintf( 'Vorlage %d ist keine entfernte Vorlage (siehe detached in lit/tb-list); nichts wurde gespeichert.', $template->id ), [ 'template_id' => 'nur Vorlagen der Website (templates) oder entfernte Vorlagen (detached) aus ' . 'lit/tb-list', ] ); } if (! $reattach) { $marked = $template->markedUnused === null ? '' : sprintf(' (seit %s als ungenutzt markiert; Divi löscht sie 7 Tage danach)', $template->markedUnused); return Errors::invalidInput( sprintf( 'Vorlage %d ist im Theme Builder nicht mehr zugeordnet%s; nichts wurde gespeichert.', $template->id, $marked ), [ 'reattach' => 'nur auf Wunsch des Nutzers mit reattach: true wieder zuordnen (die Vorlage bleibt ' . 'deaktiviert)', ] ); } if ($this->access->store()->liveId() <= 0) { return Errors::unsupported( 'Der Divi Theme Builder wurde noch nie gespeichert; es gibt keine Liste, der die Vorlage wieder ' . 'zugeordnet werden kann. Nichts wurde gespeichert.', 'Den Theme Builder einmal speichern (Divi → Theme Builder), dann erneut aufrufen.' ); } return $this->access->layoutsRefusal($template); } /** * @param list $useOn * @param list $excludeFrom */ private function dryRun(Template $template, array $useOn, array $excludeFrom): WP_Error { $overlaps = Template::overlapDetails($this->access->editableLiveTemplates(), $template->id, $useOn); return Errors::confirmRequired( sprintf( 'Vorlage „%s“ ist aktiv; die neuen Bedingungen (use_on) und Ausnahmen (exclude_from) wirken sofort auf ' . 'der Website – vorher/nachher unter before/after.%s Nichts wurde gespeichert (Probelauf).', $template->title, Template::hiddenOverlapsNote($overlaps['hidden_overlaps']) ), [ 'before' => $this->labels($template->useOn, $template->excludeFrom), 'after' => $this->labels($useOn, $excludeFrom), ] + $overlaps ); } /** * @param list $useOn * @param list $excludeFrom * @return array{use_on: list, exclude_from: list} */ private function labels(array $useOn, array $excludeFrom): array { return [ 'use_on' => array_map($this->conditions->label(...), $useOn), 'exclude_from' => array_map($this->conditions->label(...), $excludeFrom), ]; } /** * @param list $useOn * @param list $excludeFrom * @return array * @throws WriteFailed */ private function write(Template $template, array $useOn, array $excludeFrom, bool $reattach): array { $store = $this->access->store(); $themeBuilderId = $store->liveId(); try { if ($reattach) { $store->setEnabled($template->id, false); $store->attach($themeBuilderId, $template->id); foreach (array_merge([$template->id], array_column($template->layouts, 'id')) as $id) { if ($id > 0) { $store->unmarkUnused($id); } } } $store->setConditions($template->id, $useOn, $excludeFrom); } catch (Throwable $e) { $this->undo($template, $reattach, $themeBuilderId, $e); } $warnings = []; if ($template->isLive()) { FollowUp::run( static fn () => $store->clearCaches(), 'theme builder cache', 'Der Cache des Theme Builders konnte nicht geleert werden; die Bedingungen sind gespeichert, wirken ' . 'aber evtl. erst nach „Statische CSS-Dateien leeren“ (Divi → Theme-Optionen → Builder).', $warnings ); } if ($reattach) { $warnings[] = TbCreateTemplateAbility::STALE_EDITOR; } $logId = $this->log->record( $this->name(), DiviThemeBuilder::TEMPLATE, (string) $template->id, sprintf( 'Theme Builder: Bedingungen von „%s“ gesetzt%s (vorher: %s; Ausnahmen vorher: %s)', $template->title, $reattach ? sprintf( ', wieder zugeordnet (deaktiviert, vorher %s)', $template->enabled ? 'aktiviert' : 'deaktiviert' ) : '', self::listed($template->useOn), self::listed($template->excludeFrom) ) ); $after = $store->template($template->id, $template->attached || $reattach); $live = $after?->isLive() ?? false; return ToolResult::ok(sprintf( 'Bedingungen der Vorlage „%s“ gesetzt%s%s.', $template->title, $reattach ? ' und wieder zugeordnet' : '', $live ? ' – wirkt sofort auf der Website; zurück mit lit/tb-assign und den Bedingungen unter previous, ' . 'ausblenden mit lit/tb-activate (enabled: false)' : ' (Vorlage deaktiviert, ohne Wirkung auf die Website)' )) ->with('template_id', $template->id) ->with('template_rev', $after?->rev() ?? '') ->with('live', $live) ->with('previous', ['use_on' => $template->useOn, 'exclude_from' => $template->excludeFrom] + ($reattach ? ['enabled' => $template->enabled] : [])) ->warnings($warnings) ->logId($logId) ->urls(null, (string) admin_url('admin.php?page=et_theme_builder')) ->toArray(); } /** @param list $conditions */ private static function listed(array $conditions): string { return $conditions === [] ? 'keine' : implode(', ', $conditions); } /** @throws WriteFailed Always. */ private function undo(Template $template, bool $reattach, int $themeBuilderId, Throwable $cause): never { $store = $this->access->store(); $steps = [static fn () => $store->setConditions($template->id, $template->useOn, $template->excludeFrom)]; if ($reattach) { $steps[] = static fn () => $store->detach($themeBuilderId, $template->id); $steps[] = static fn () => $store->setEnabled($template->id, $template->enabled); } $failures = Rollback::attempt(...$steps); if ($failures === []) { throw WriteFailed::rolledBack(self::REASON, $cause, 'tb-assign failed: ' . $cause->getMessage()); } $this->log->recordPartial( $this->name(), DiviThemeBuilder::TEMPLATE, (string) $template->id, sprintf( 'Theme Builder: Bedingungen von „%s“ nur teilweise zurückgenommen (%s)', $template->title, implode('; ', $failures) ) ); throw WriteFailed::partial( self::REASON, $cause, 'tb-assign rollback: ' . implode('; ', $failures), $template->id, hint: sprintf( 'Mit lit/tb-list prüfen; vorher galt: %s; Ausnahmen: %s.', self::listed($template->useOn), self::listed($template->excludeFrom) ) ); } }