dbxapp 4.1.3
CMS, Shop, Workflows und modulare Geschäftsanwendungen
Loading...
Searching...
No Matches
dbxKiDesignService.class.php
Go to the documentation of this file.
1<?php
2namespace dbx\dbxKi;
3
13
14 private const SESSION_KEY = 'design_jobs';
15 private const RESULT_CONTRACT = 'dbx.design.result.v1';
16
18 private $designService;
19 private $texts;
20
21 public function __construct() {
22 $this->designService = dbx()->get_include_obj('dbxDesignService', 'dbxDesign_admin');
23 }
24
25 private function h($value): string {
26 return htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8');
27 }
28
29 private function texts() {
30 if ($this->texts) {
31 return $this->texts;
32 }
33 dbx()->get_system_obj('dbxForm', 'use');
34 $texts = new \dbxForm();
35 $texts->init('ki-design-texts');
36 $texts->_fd = 'dbxKi|design-briefing';
37 $texts->load_fd_messages();
38 $texts->set_form_help_enabled(false);
39 $this->texts = $texts;
40 return $this->texts;
41 }
42
43 private function message(string $key, array $replacements = array()): string {
44 return $replacements
45 ? $this->texts()->format_fd_message($key, $replacements)
46 : $this->texts()->get_fd_message($key);
47 }
48
49 private function url(string $run1, array $params = array()): string {
50 $url = '?dbx_modul=dbxKi&dbx_run1=' . rawurlencode($run1);
51 foreach ($params as $key => $value) {
52 if ($value !== '') {
53 $url .= '&' . rawurlencode((string)$key) . '=' . rawurlencode((string)$value);
54 }
55 }
56 return $url;
57 }
58
59 private function designOptions(): array {
60 $items = array();
61 foreach ($this->designService->listDesigns() as $name => $meta) {
62 $items[$name] = $meta['title'] . ' (' . $name . ')';
63 }
64 return $items;
65 }
66
67 private function briefingForm(string $template = 'ki-design-briefing') {
68 $form = dbx()->get_system_obj('dbxForm');
69 $form->init('ki-design-briefing', $template);
70 $form->_fd = 'dbxKi|design-briefing';
71 $form->load_fd_messages();
72 $form->set_form_help_enabled(false);
73 $form->_action = $this->url('briefing_design_export');
74 $form->_msg_info = '';
75 $selected = $this->designService->normalizeName((string)dbx()->get_request_var('design', 'dbxapp', 'parameter'));
76 if (!isset($this->designService->listDesigns()[$selected])) {
77 $selected = 'dbxapp';
78 }
79 $mode = strtolower((string)dbx()->get_request_var('mode', 'update', 'parameter')) === 'create' ? 'create' : 'update';
80 $form->_data = array_merge($form->_data, array(
81 'mode' => $mode,
82 'source_design' => $selected,
83 'target_design' => $mode === 'update' ? $selected : '',
84 'goal' => '',
85 'layout_notes' => '',
86 'menu_notes' => '',
87 'branding_notes' => '',
88 'footer_notes' => '',
89 'responsive_notes' => '',
90 'preserve_behavior' => 1,
91 ));
92 $texts = $this->texts();
93 $form->add_fld('mode', 'select-single-label', label: $texts->get_fd_message('label_mode'), rules: 'parameter', options: array(
94 'update' => $texts->get_fd_message('mode_update'),
95 'create' => $texts->get_fd_message('mode_create'),
96 ), dd: '');
97 $form->add_fld('source_design', 'select-single-label', label: $texts->get_fd_message('label_source_design'), rules: 'parameter|max=63', options: $this->designOptions(), dd: '');
98 $form->add_fld('target_design', 'text-label', label: $texts->get_fd_message('label_target_design'), rules: 'parameter+_-|min=2|max=63', placeholder: $texts->get_fd_message('placeholder_target_design'), dd: '');
99 $form->add_fld('goal', 'textarea-label', label: $texts->get_fd_message('label_goal'), rules: 'varchar|min=10|max=6000', data: 'rows=7', placeholder: $texts->get_fd_message('placeholder_goal'), dd: '');
100 $form->add_fld('layout_notes', 'textarea-label', label: $texts->get_fd_message('label_layout_notes'), rules: 'varchar|max=1800', data: 'rows=3', placeholder: $texts->get_fd_message('placeholder_layout'), dd: '');
101 $form->add_fld('menu_notes', 'textarea-label', label: $texts->get_fd_message('label_menu_notes'), rules: 'varchar|max=1800', data: 'rows=3', placeholder: $texts->get_fd_message('placeholder_menu'), dd: '');
102 $form->add_fld('branding_notes', 'textarea-label', label: $texts->get_fd_message('label_branding_notes'), rules: 'varchar|max=1800', data: 'rows=3', placeholder: $texts->get_fd_message('placeholder_branding'), dd: '');
103 $form->add_fld('footer_notes', 'textarea-label', label: $texts->get_fd_message('label_footer_notes'), rules: 'varchar|max=1800', data: 'rows=3', placeholder: $texts->get_fd_message('placeholder_footer'), dd: '');
104 $form->add_fld('responsive_notes', 'textarea-label', label: $texts->get_fd_message('label_responsive_notes'), rules: 'varchar|max=1800', data: 'rows=3', placeholder: $texts->get_fd_message('placeholder_responsive'), dd: '');
105 $form->add_fld('preserve_behavior', 'checkbox-label', label: $texts->get_fd_message('label_preserve_behavior'), rules: 'int', dd: '');
106 return $form;
107 }
108
112 public function renderBriefing(): string {
113 // dbxForm ist ein Request-Singleton. Den kompakten Importblock zuerst
114 // vollständig rendern und erst danach das eigentliche Briefing
115 // initialisieren, damit beide Formulare getrennte IDs und Token behalten.
116 $importPanel = $this->renderImportPanel();
117 $form = $this->briefingForm();
118 $form->add_rep('design_list_url', $this->h('?dbx_modul=dbxDesign_admin&dbx_run1=list'));
119 $form->add_rep('import_panel', $importPanel);
120 $form->add_rep('bar_title', $this->message('bar_title'));
121 $form->add_rep('bar_subtitle', $this->message('bar_subtitle'));
122 $form->add_rep('bar_icon', 'bi-stars');
123 $form->add_rep('bar_actions', '<a class="btn btn-outline-secondary btn-sm" href="' . $this->h('?dbx_modul=dbxDesign_admin&dbx_run1=list') . '"><i class="bi bi-palette"></i> ' . $this->h($this->message('action_design_studio')) . '</a>');
124 foreach ($this->barDefaults() as $key => $value) {
125 $form->add_rep($key, $value);
126 }
127 return $form->run();
128 }
129
133 public function handleExport(): void {
134 $form = $this->briefingForm();
135 if (!$form->submit()) {
136 throw new \RuntimeException($this->message('token_error'));
137 }
138 $in = $this->collectBriefing($form);
139 $sourceDir = $this->designService->designDir($in['source_design']);
140 if (!is_dir($sourceDir)) {
141 throw new \RuntimeException($this->message('source_not_found'));
142 }
143 if ($in['mode'] === 'update') {
144 $in['target_design'] = $in['source_design'];
145 } elseif (!$this->designService->isValidName($in['target_design'])) {
146 throw new \InvalidArgumentException($this->message('target_required'));
147 } elseif (isset($this->designService->listDesigns()[$in['target_design']])) {
148 throw new \InvalidArgumentException($this->message('target_exists'));
149 }
150 if (trim($in['goal']) === '') {
151 throw new \InvalidArgumentException($this->message('goal_required'));
152 }
153 if (!class_exists('ZipArchive')) {
154 throw new \RuntimeException($this->message('zip_unavailable'));
155 }
156
157 $tmp = tempnam(sys_get_temp_dir(), 'dbx-design-briefing-');
158 $zip = new \ZipArchive();
159 if ($tmp === false || $zip->open($tmp, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) {
160 throw new \RuntimeException($this->message('briefing_zip_error'));
161 }
162 $manifest = array(
163 'contract' => 'dbx.design.briefing.v1',
164 'result_contract' => self::RESULT_CONTRACT,
165 'mode' => $in['mode'],
166 'source_design' => $in['source_design'],
167 'target_design' => $in['target_design'],
168 'created_at' => date(DATE_ATOM),
169 'required_result_root' => 'result/design/',
170 );
171 $context = array(
172 'design' => $this->designService->readMetadata($in['source_design']),
173 'validation' => $this->designService->validateDesignDirectory($sourceDir, false),
174 'files' => array_keys($this->designService->relativeFiles($sourceDir)),
175 'runtime' => array(
176 'content_slot' => '[dbx:content]',
177 'design_slots' => array('[dbx:logo]', '[dbx:branding]', '[dbx:footer]'),
178 'template_engine' => 'dbxTPL',
179 'admin_module' => 'dbxDesign_admin',
180 ),
181 );
182 $zip->addFromString('00-START.md', $this->startInstructions($in));
183 $zip->addFromString('manifest.json', json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
184 $zip->addFromString('briefing.json', json_encode($in, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
185 $zip->addFromString('context.json', json_encode($context, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
186 $zip->addFromString('KI-AUFTRAG.md', $this->aiInstructions($in));
187 $zip->addFromString('RESULT-FORMAT.md', $this->resultFormat());
188 foreach ($this->designService->relativeFiles($sourceDir) as $relative => $absolute) {
189 $zip->addFile($absolute, 'source-design/' . $in['source_design'] . '/' . $relative);
190 }
191 $zip->close();
192 $name = 'dbxki-design-' . $in['mode'] . '-' . $in['target_design'] . '.zip';
193 header('Content-Type: application/zip');
194 header('Content-Disposition: attachment; filename="' . $name . '"');
195 header('Content-Length: ' . filesize($tmp));
196 readfile($tmp);
197 @unlink($tmp);
198 exit;
199 }
200
201 private function collectBriefing($form): array {
202 $rules = array(
203 'mode' => 'parameter',
204 'source_design' => 'parameter|max=63',
205 'target_design' => 'parameter+_-|max=63',
206 'goal' => 'varchar|max=6000',
207 'layout_notes' => 'varchar|max=1800',
208 'menu_notes' => 'varchar|max=1800',
209 'branding_notes' => 'varchar|max=1800',
210 'footer_notes' => 'varchar|max=1800',
211 'responsive_notes' => 'varchar|max=1800',
212 'preserve_behavior' => 'int',
213 );
214 $out = array();
215 foreach ($rules as $name => $rule) {
216 $out[$name] = $form->get_fld_value($name, '', $rule);
217 }
218 $out['mode'] = $out['mode'] === 'create' ? 'create' : 'update';
219 $out['source_design'] = $this->designService->normalizeName($out['source_design']);
220 $out['target_design'] = $this->designService->normalizeName($out['target_design']);
221 return $out;
222 }
223
224 private function startInstructions(array $in): string {
225 return "# dbxKi Design-Auftrag\n\n"
226 . "1. Lies `KI-AUFTRAG.md`, `briefing.json`, `context.json` und das komplette Ausgangsdesign.\n"
227 . "2. Bearbeite ausschließlich Design-Dateien. Keine Fachlogik, Datenbank, Rechte oder Module ändern.\n"
228 . "3. Lege nur neue/geänderte Dateien unter `result/design/` ab.\n"
229 . "4. Erzeuge `manifest.json` im Antwort-ZIP nach `RESULT-FORMAT.md`.\n"
230 . "5. Ergebnis als ZIP zurückgeben. Ziel: `" . $in['target_design'] . "`.\n";
231 }
232
233 private function aiInstructions(array $in): string {
234 return "# Verbindlicher Design-Auftrag\n\n"
235 . "## Ziel\n\n" . $in['goal'] . "\n\n"
236 . "## Aufteilung\n\n" . ($in['layout_notes'] ?: '(aus dem Ziel ableiten)') . "\n\n"
237 . "## Menü\n\n" . ($in['menu_notes'] ?: '(bestehende Navigation kompatibel weiterverwenden)') . "\n\n"
238 . "## Branding\n\n" . ($in['branding_notes'] ?: '(bestehendes Branding behutsam weiterentwickeln)') . "\n\n"
239 . "## Footer\n\n" . ($in['footer_notes'] ?: '(Fenster-Dock und Rechtelinks funktionsfähig erhalten)') . "\n\n"
240 . "## Mobil / Barrierefreiheit\n\n" . ($in['responsive_notes'] ?: 'Responsive, tastaturbedienbar und kontrastreich umsetzen.') . "\n\n"
241 . "## Nicht verhandelbar\n\n"
242 . "- `[dbx:content]` genau einmal erhalten.\n"
243 . "- `{dbx:title}`, `{dbx:design}`, `{dbx:skin_css}`, `{dbx:skin_class}` und `core.js?design={dbx:design}` erhalten.\n"
244 . "- dbxMenu-Aufrufe, Admin-Menü, Ajax, openWin, Fenster-Dock, dbxForm und dbxReport bleiben funktionsfähig.\n"
245 . "- Optionale Bereiche über `[dbx:logo]`, `[dbx:branding]`, `[dbx:footer]` und die gleichnamigen Dateien unter `htm/` strukturieren.\n"
246 . "- Keine PHP-Dateien, keine Datenbank, keine DD/FD, keine Module und keine externen Build-Abhängigkeiten liefern.\n"
247 . "- Das Paket bleibt eigenständig; keine privaten Dateien eines anderen Designs referenzieren.\n";
248 }
249
250 private function resultFormat(): string {
251 return "# Antwort-ZIP\n\n"
252 . "```text\nmanifest.json\nresult/design/htm/default.htm\nresult/design/css/design-custom.css\n...\n```\n\n"
253 . "`manifest.json`:\n\n"
254 . "```json\n{\n \"contract\": \"" . self::RESULT_CONTRACT . "\",\n"
255 . " \"mode\": \"create|update\",\n \"source_design\": \"dbxapp\",\n \"target_design\": \"zielname\",\n"
256 . " \"summary\": \"Kurze Beschreibung\"\n}\n```\n\n"
257 . "Bei `update` nur neue/geänderte Dateien liefern. Bei `create` darf ebenfalls ein Delta zum mitgelieferten Ausgangsdesign geliefert werden. dbxapp baut daraus im Staging ein vollständiges Paket und prüft es vor der Freigabe.\n";
258 }
259
260 private function importForm() {
261 $form = dbx()->get_system_obj('dbxForm');
262 $form->init('ki-design-result-import', 'ki-design-import');
263 $form->_fd = 'dbxKi|design-briefing';
264 $form->load_fd_messages();
265 $form->set_form_help_enabled(false);
266 $form->_action = $this->url('design_bundle_import');
267 $form->_msg_info = '';
268 return $form;
269 }
270
274 public function renderImportPanel(): string {
275 return $this->importForm()->run();
276 }
277
281 public function handleImport(): string {
282 $form = $this->importForm();
283 try {
284 if (!$form->submit()) {
285 throw new \RuntimeException($this->message('token_error'));
286 }
287 $upload = is_array($_FILES['design_zip'] ?? null) ? $_FILES['design_zip'] : array();
288 $state = $this->extractResult($upload);
289 $token = bin2hex(random_bytes(12));
290 if (!isset($_SESSION['dbx']['dbxKi'][self::SESSION_KEY]) || !is_array($_SESSION['dbx']['dbxKi'][self::SESSION_KEY])) {
291 $_SESSION['dbx']['dbxKi'][self::SESSION_KEY] = array();
292 }
293 $_SESSION['dbx']['dbxKi'][self::SESSION_KEY][$token] = $state;
294 return $this->renderPreview($token, $state);
295 } catch (\Throwable $e) {
296 return dbx()->get_system_obj('dbxTPL')->get_tpl('dbxKi|ki-design-result', array(
297 'result_class' => 'alert-danger',
298 'result_title' => $this->message('import_failed_title'),
299 'result_message' => $this->h($e->getMessage()),
300 'result_actions' => '<a class="btn btn-outline-secondary" href="' . $this->h($this->url('briefing_design')) . '">' . $this->h($this->message('action_back')) . '</a>',
301 ));
302 }
303 }
304
305 private function extractResult(array $file): array {
306 if (($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
307 throw new \InvalidArgumentException($this->message('zip_select'));
308 }
309 $tmp = (string)($file['tmp_name'] ?? '');
310 if ($tmp === '' || !is_uploaded_file($tmp)) {
311 throw new \InvalidArgumentException($this->message('zip_upload_invalid'));
312 }
313 $maxBytes = max(1, (int)dbx()->get_config('dbxKi', 'max_bundle_bytes', 52428800));
314 if ((int)($file['size'] ?? 0) > $maxBytes) {
315 throw new \InvalidArgumentException($this->message('zip_too_large'));
316 }
317 if (!class_exists('ZipArchive')) {
318 throw new \RuntimeException($this->message('zip_unavailable'));
319 }
320 $work = dbx()->os_path(rtrim(dbx()->get_file_dir(), '/\\') . '/tmp/ki-design/' . bin2hex(random_bytes(10)));
321 if (!mkdir($work, 0777, true) && !is_dir($work)) {
322 throw new \RuntimeException($this->message('staging_error'));
323 }
324 try {
325 $zip = new \ZipArchive();
326 if ($zip->open($tmp) !== true) {
327 throw new \InvalidArgumentException($this->message('zip_open_error'));
328 }
329 $total = 0;
330 $maxFiles = max(50, (int)dbx()->get_config('dbxKi', 'max_bundle_files', 50));
331 if ($zip->numFiles > $maxFiles) {
332 $zip->close();
333 throw new \InvalidArgumentException($this->message('zip_too_many_files'));
334 }
335 for ($i = 0; $i < $zip->numFiles; $i++) {
336 $name = str_replace('\\', '/', (string)$zip->getNameIndex($i));
337 $stat = $zip->statIndex($i);
338 $total += (int)($stat['size'] ?? 0);
339 if ($name === '' || $name[0] === '/' || strpos($name, '..') !== false || strpos($name, ':') !== false || strpos($name, "\0") !== false) {
340 $zip->close();
341 throw new \InvalidArgumentException($this->message('zip_path_invalid', array('path' => $name)));
342 }
343 if ($total > $maxBytes) {
344 $zip->close();
345 throw new \InvalidArgumentException($this->message('zip_unpacked_too_large'));
346 }
347 }
348 if (!$zip->extractTo($work)) {
349 $zip->close();
350 throw new \RuntimeException($this->message('zip_extract_error'));
351 }
352 $zip->close();
353 $root = $this->findResultRoot($work);
354 $manifestFile = $root . DIRECTORY_SEPARATOR . 'manifest.json';
355 $resultDir = $root . DIRECTORY_SEPARATOR . 'result' . DIRECTORY_SEPARATOR . 'design';
356 $manifest = is_file($manifestFile) ? json_decode((string)file_get_contents($manifestFile), true) : null;
357 if (!is_array($manifest) || ($manifest['contract'] ?? '') !== self::RESULT_CONTRACT) {
358 throw new \InvalidArgumentException($this->message('manifest_contract_error', array('contract' => self::RESULT_CONTRACT)));
359 }
360 if (!is_dir($resultDir)) {
361 throw new \InvalidArgumentException($this->message('result_directory_missing'));
362 }
363 $rawMode = strtolower(trim((string)($manifest['mode'] ?? '')));
364 if (!in_array($rawMode, array('create', 'update'), true)) {
365 throw new \InvalidArgumentException($this->message('manifest_mode_error'));
366 }
367 $mode = $rawMode;
368 $source = $this->designService->normalizeName((string)($manifest['source_design'] ?? ''));
369 $target = $this->designService->normalizeName((string)($manifest['target_design'] ?? ''));
370 if (!$this->designService->isValidName($source) || !$this->designService->isValidName($target)) {
371 throw new \InvalidArgumentException($this->message('manifest_design_error'));
372 }
373 $designs = $this->designService->listDesigns();
374 if (!isset($designs[$source])) {
375 throw new \InvalidArgumentException($this->message('manifest_source_missing'));
376 }
377 if ($mode === 'create' && isset($designs[$target])) {
378 throw new \InvalidArgumentException($this->message('manifest_target_exists'));
379 }
380 if ($mode === 'update' && !isset($designs[$target])) {
381 throw new \InvalidArgumentException($this->message('manifest_target_missing'));
382 }
383 $files = $this->designService->relativeFiles($resultDir);
384 if (!$files) {
385 throw new \InvalidArgumentException($this->message('result_empty'));
386 }
387 foreach ($files as $relative => $absolute) {
388 $fileError = $this->designService->validateResultFile($relative, $absolute);
389 if ($fileError !== '') {
390 throw new \InvalidArgumentException($this->message('result_file_invalid', array('file' => $relative)));
391 }
392 }
393 } catch (\Throwable $e) {
394 $this->removeWorkDir($work);
395 throw $e;
396 }
397 return array(
398 'work_dir' => $work,
399 'result_dir' => $resultDir,
400 'manifest' => $manifest,
401 'mode' => $mode,
402 'source_design' => $source,
403 'target_design' => $target,
404 'files' => array_keys($files),
405 );
406 }
407
408 private function findResultRoot(string $work): string {
409 if (is_file($work . '/manifest.json')) {
410 return $work;
411 }
412 $children = glob($work . '/*', GLOB_ONLYDIR) ?: array();
413 if (count($children) === 1 && is_file($children[0] . '/manifest.json')) {
414 return $children[0];
415 }
416 return $work;
417 }
418
419 private function renderPreview(string $token, array $state): string {
420 $targetBase = is_dir($this->designService->designDir($state['target_design']))
421 ? $this->designService->designDir($state['target_design'])
422 : $this->designService->designDir($state['source_design']);
423 $rows = '';
424 foreach ($state['files'] as $relative) {
425 $new = $state['result_dir'] . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $relative);
426 $old = $targetBase . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $relative);
427 $statusKey = !is_file($old) ? 'new' : (hash_file('sha256', $old) === hash_file('sha256', $new) ? 'unchanged' : 'changed');
428 $class = $statusKey === 'new' ? 'text-bg-success' : ($statusKey === 'changed' ? 'text-bg-warning' : 'text-bg-secondary');
429 $rows .= '<tr><td><code>' . $this->h($relative) . '</code></td><td><span class="badge ' . $class . '">' . $this->h($this->message('status_' . $statusKey)) . '</span></td><td class="text-end">' . number_format((int)filesize($new), 0, ',', '.') . ' B</td></tr>';
430 }
431 $form = dbx()->get_system_obj('dbxForm');
432 $form->init('ki-design-result-apply', 'ki-design-preview');
433 $form->_fd = 'dbxKi|design-briefing';
434 $form->load_fd_messages();
435 $form->set_form_help_enabled(false);
436 $form->_action = $this->url('design_bundle_apply');
437 $form->_msg_info = '';
438 $form->_data['job_token'] = $token;
439 $form->add_fld('job_token', 'dbx|hidden', rules: 'parameter', dd: '');
440 $form->add_rep('mode', $this->h($state['mode']));
441 $form->add_rep('source_design', $this->h($state['source_design']));
442 $form->add_rep('target_design', $this->h($state['target_design']));
443 $form->add_rep('summary', $this->h((string)($state['manifest']['summary'] ?? '')));
444 $form->add_rep('file_count', count($state['files']));
445 $form->add_rep('file_rows', $rows);
446 $form->add_rep('back_url', $this->h($this->url('briefing_design')));
447 $form->add_rep('bar_title', $this->message('preview_title'));
448 $form->add_rep('bar_subtitle', $this->h($state['mode'] . ': ' . $state['source_design'] . ' → ' . $state['target_design']));
449 $form->add_rep('bar_icon', 'bi-search');
450 $form->add_rep('bar_actions', '');
451 foreach ($this->barDefaults() as $key => $value) {
452 $form->add_rep($key, $value);
453 }
454 return $form->run();
455 }
456
457 private function barDefaults(): array {
458 return array(
459 'bar_class' => 'dbx-module-bar',
460 'bar_title_class' => 'dbx-module-bar-titleblock',
461 'bar_title_pre' => '',
462 'bar_title_heading_attrs' => '',
463 'bar_middle' => '',
464 'bar_extra' => '',
465 'bar_actions_class' => 'dbx-module-bar-actions',
466 );
467 }
468
472 public function handleApply(): string {
473 $form = dbx()->get_system_obj('dbxForm');
474 $form->init('ki-design-result-apply', 'ki-design-preview');
475 $form->_fd = 'dbxKi|design-briefing';
476 $form->load_fd_messages();
477 $form->set_form_help_enabled(false);
478 $form->_action = $this->url('design_bundle_apply');
479 $form->_msg_info = '';
480 $form->add_fld('job_token', 'dbx|hidden', rules: 'parameter', dd: '');
481 try {
482 if (!$form->submit()) {
483 throw new \RuntimeException($this->message('token_error'));
484 }
485 $token = (string)$form->get_fld_value('job_token', '', 'parameter');
486 $state = $_SESSION['dbx']['dbxKi'][self::SESSION_KEY][$token] ?? null;
487 if (!is_array($state)) {
488 throw new \RuntimeException($this->message('preview_expired'));
489 }
490 $result = $this->designService->applyResult(
491 $state['source_design'],
492 $state['target_design'],
493 $state['result_dir'],
494 $state['mode']
495 );
496 $this->removeWorkDir((string)($state['work_dir'] ?? ''));
497 unset($_SESSION['dbx']['dbxKi'][self::SESSION_KEY][$token]);
498 $backup = (string)($result['backup'] ?? '');
499 $message = $this->message('apply_message', array('name' => $result['name']));
500 if ($backup !== '') {
501 $message .= ' ' . $this->message('backup_message', array('backup' => basename($backup)));
502 }
503 return dbx()->get_system_obj('dbxTPL')->get_tpl('dbxKi|ki-design-result', array(
504 'result_class' => 'alert-success',
505 'result_title' => $this->message('apply_success_title'),
506 'result_message' => $this->h($message),
507 'result_actions' => '<a class="btn btn-primary" target="_blank" href="' . $this->h('?dbx_design=' . rawurlencode($result['name'])) . '"><i class="bi bi-eye"></i> ' . $this->h($this->message('action_view_design')) . '</a> '
508 . '<a class="btn btn-outline-secondary" href="' . $this->h('?dbx_modul=dbxDesign_admin&dbx_run1=list') . '">' . $this->h($this->message('action_design_studio')) . '</a>',
509 ));
510 } catch (\Throwable $e) {
511 return dbx()->get_system_obj('dbxTPL')->get_tpl('dbxKi|ki-design-result', array(
512 'result_class' => 'alert-danger',
513 'result_title' => $this->message('apply_error_title'),
514 'result_message' => $this->h($e->getMessage()),
515 'result_actions' => '<a class="btn btn-outline-secondary" href="' . $this->h($this->url('briefing_design')) . '">' . $this->h($this->message('action_back')) . '</a>',
516 ));
517 }
518 }
519
520 private function removeWorkDir(string $dir): void {
521 if ($dir === '' || !is_dir($dir)) {
522 return;
523 }
524 $root = rtrim(str_replace('\\', '/', dbx()->os_path(rtrim(dbx()->get_file_dir(), '/\\') . '/tmp/ki-design')), '/');
525 $normalized = rtrim(str_replace('\\', '/', dbx()->os_path($dir)), '/');
526 if ($normalized === $root || strpos($normalized, $root . '/') !== 0) {
527 return;
528 }
529 $it = new \RecursiveIteratorIterator(
530 new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS),
531 \RecursiveIteratorIterator::CHILD_FIRST
532 );
533 foreach ($it as $file) {
534 if ($file->isDir()) {
535 @rmdir($file->getPathname());
536 } else {
537 @unlink($file->getPathname());
538 }
539 }
540 @rmdir($dir);
541 }
542}
543?>
dbxKi-Vertrag für Design-Briefing, Antwort-ZIP, Vorschau und Anwendung.
handleApply()
Wendet eine zuvor geprüfte Antwort nach dbxForm-Bestätigung an.
handleImport()
Entpackt und validiert eine Design-Antwort in ein isoliertes Staging.
renderBriefing()
Rendert das Design-Briefing als dbxForm.
handleExport()
Prüft das Briefing und sendet ein vollständiges KI-Auftragspaket.
renderImportPanel()
Rendert den kompakten Importblock für Design-Antworten.
os_path(string $path)
Normalisiert einen Pfad fuer das aktuelle Betriebssystem.
Definition dbxApi.php:1538
get_file_dir()
Liefert das files/-Verzeichnis der Installation.
Definition dbxApi.php:1524
if(!defined( 'IMG_WEBP')) define( 'IMG_WEBP'
exit
Definition index.php:146
if( $demoId<=0) foreach(array('create_date', 'create_uid', 'update_date', 'update_uid', 'owner',) as $systemField) $items
DBX schema administration.