dbxapp 4.1.3
CMS, Shop, Workflows und modulare Geschäftsanwendungen
Loading...
Searching...
No Matches
dbxKiBundleService.class.php
Go to the documentation of this file.
1<?php
2namespace dbx\dbxKi;
3
4use dbx\dbxContent\dbxContentLng;
5
6require_once dirname(__DIR__, 2) . '/dbxContent/include/dbxContent_bootstrap_sync.php';
7
9
10 private const BUNDLE_VERSION = '1.0';
11 private const SESSION_KEY = 'bundle_jobs';
12 private const AREA = 'cms';
13
14 private function cms(): dbxKiCmsService {
15 return dbx()->get_include_obj('dbxKiCmsService', 'dbxKi');
16 }
17
18 private function help(): dbxKiHelp {
19 return dbx()->get_include_obj('dbxKiHelp', 'dbxKi');
20 }
21
22 private function withModuleBar(array $data, string $screen, string $actionsHtml = '', string $barTitle = ''): array {
23 return array_merge($data, $this->help()->moduleBarTemplateData($screen, $actionsHtml, $barTitle));
24 }
25
26 private function moduleUrl(string $run1, array $params = array()): string {
27 $url = '?dbx_modul=dbxKi&dbx_run1=' . rawurlencode($run1);
28 foreach ($params as $key => $value) {
29 if ($value === null || $value === '') {
30 continue;
31 }
32 $url .= '&' . rawurlencode((string)$key) . '=' . rawurlencode((string)$value);
33 }
34 return $url;
35 }
36
37 private function esc($value): string {
38 return htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8');
39 }
40
41 private function configInt(string $key, int $default): int {
42 return max(1, (int)dbx()->get_config('dbxKi', $key, $default));
43 }
44
45 private function sessionBucket(): array {
46 if (!isset($_SESSION['dbx']['dbxKi']) || !is_array($_SESSION['dbx']['dbxKi'])) {
47 $_SESSION['dbx']['dbxKi'] = array();
48 }
49 if (!isset($_SESSION['dbx']['dbxKi'][self::SESSION_KEY]) || !is_array($_SESSION['dbx']['dbxKi'][self::SESSION_KEY])) {
50 $_SESSION['dbx']['dbxKi'][self::SESSION_KEY] = array();
51 }
52 return $_SESSION['dbx']['dbxKi'][self::SESSION_KEY];
53 }
54
55 private function getJob(string $token): array {
56 $token = $this->sanitizeToken($token);
57 if ($token === '') {
58 return array();
59 }
60 $bucket = $this->sessionBucket();
61 return is_array($bucket[$token] ?? null) ? $bucket[$token] : array();
62 }
63
64 private function setJob(string $token, array $state): array {
65 $token = $this->sanitizeToken($token);
66 if ($token === '') {
67 return array();
68 }
69 $state['proc_key'] = $token;
70 $state['updated_at'] = date('Y-m-d H:i:s');
71 $_SESSION['dbx']['dbxKi'][self::SESSION_KEY][$token] = $state;
72 return $state;
73 }
74
75 private function sanitizeToken(string $token): string {
76 return preg_replace('/[^A-Za-z0-9_-]+/', '', (string)$token);
77 }
78
79 private function newToken(): string {
80 return substr(md5(session_id() . microtime(true) . mt_rand()), 0, 16);
81 }
82
83 private function tempRoot(): string {
84 $dir = rtrim(dbx()->get_file_dir(), '/\\') . '/tmp/ki-bundle';
85 $dir = dbx()->os_path($dir);
86 if (!is_dir($dir)) {
87 @mkdir($dir, 0777, true);
88 }
89 return $dir;
90 }
91
92 private function jobDir(string $token): string {
93 return rtrim($this->tempRoot(), '/\\') . DIRECTORY_SEPARATOR . $this->sanitizeToken($token);
94 }
95
96 private function removeJobDir(string $token): void {
97 $dir = $this->jobDir($token);
98 if (!is_dir($dir)) {
99 return;
100 }
101 $it = new \RecursiveIteratorIterator(
102 new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS),
103 \RecursiveIteratorIterator::CHILD_FIRST
104 );
105 foreach ($it as $file) {
106 if ($file->isDir()) {
107 @rmdir($file->getPathname());
108 } else {
109 @unlink($file->getPathname());
110 }
111 }
112 @rmdir($dir);
113 }
114
115 public function describeBundle(): array {
116 $cms = $this->cms();
117 return array(
118 'ok' => 1,
119 'bundle_version' => self::BUNDLE_VERSION,
120 'api_version' => '0.1',
121 'area' => self::AREA,
122 'module' => 'dbxKi',
123 'purpose' => 'Offline KI-Bundles fuer CMS-Aenderungen ohne direkten API-Zugriff.',
124 'upload_url' => $this->moduleUrl('bundle_import'),
125 'export_url' => $this->moduleUrl('bundle_export'),
126 'allowed_actions' => $this->allowedActions(),
127 'files' => array(
128 'manifest.json' => 'Metadaten (title, recipe, lng, intent, auto_execute)',
129 'job.json' => 'Geordnete steps mit action und params',
130 'context.json' => 'Optionaler Arbeitskontext',
131 'assets/' => 'Optionale Bilder und Dateien',
132 'README.md' => 'Kurzbeschreibung fuer Menschen',
133 ),
134 'refs' => array(
135 'syntax' => '$ref:{step_id}.{field}',
136 'examples' => array('$ref:hero.id', '$ref:page.id', '$ref:hero_assign.usage_id'),
137 ),
138 'asset_ref' => 'In media.create_base64: asset_ref = Dateiname relativ zu assets/ (z.B. hero.jpg). Kein data_base64.',
139 'recipes' => array(
140 'page.create.v1' => 'Medien hochladen, Seite anlegen, Hero/Gallery zuweisen',
141 'page.update.v1' => 'Seite per id patchen',
142 'translation.v1' => 'translation.apply',
143 ),
144 'automation' => array(
145 'default' => 'dbxKi importiert ZIP/JSON, validiert alle Steps und zeigt die Vorschau.',
146 'auto_execute' => 'Wenn manifest.auto_execute=true gesetzt ist, startet dbxKi nach erfolgreicher Validierung automatisch den Bundle-Prozess.',
147 'ki_contract' => 'Die KI liefert nur manifest.json, job.json und optionale assets/. Keine externen Runner oder eigenen Tools.',
148 ),
149 'cms' => $cms->bundleSystemDescribe(),
150 );
151 }
152
153 private function allowedActions(): array {
154 $out = array();
155 foreach ($this->cms()->bundleActionCatalog() as $action => $meta) {
156 if ($this->cms()->bundleIsAllowedInPackage($action)) {
157 $out[$action] = $meta;
158 }
159 }
160 return $out;
161 }
162
171 private function importForm(string $template, string $returnRun1 = '') {
172 $form = dbx()->get_system_obj('dbxForm');
173 $form->init('ki-bundle-import', $template);
174 $form->_action = $this->moduleUrl('bundle_import');
175 // Nicht das gesamte Array ersetzen: dbxForm hat dort bereits sein
176 // Security-Token abgelegt.
177 $form->_data['return_run1'] = $this->sanitizeReturnRun1($returnRun1);
178 $form->_data['bundle_zip'] = '';
179 $form->_msg_info = '';
180 $form->add_fld('return_run1', 'dbx|hidden', rules: 'parameter', dd: '');
181 $form->add_fld(
182 'bundle_zip',
183 'dbxKi|ki-bundle-zip-upload',
184 label: $template === 'ki-briefing-import-panel' ? '3 KI-Antwort-ZIP' : 'Antwort-ZIP',
185 rules: '',
186 tooltip: 'ZIP-Datei mit manifest.json und job.json auswaehlen.',
187 errormsg: 'Bitte eine gueltige ZIP-Datei auswaehlen.',
188 dd: ''
189 );
190 return $form;
191 }
192
196 public function renderImportPanel(string $returnRun1): string {
197 return $this->importForm('ki-briefing-import-panel', $returnRun1)->run();
198 }
199
203 public function renderStartPage(): string {
204 $form = $this->importForm('ki-bundle-start');
205 foreach ($this->withModuleBar(array(
206 'api_url' => $this->esc($this->moduleUrl('api')),
207 'describe_url' => $this->esc($this->moduleUrl('bundle_describe')),
208 'export_url' => $this->esc($this->moduleUrl('bundle_export')),
209 'bundle_version' => $this->esc(self::BUNDLE_VERSION),
210 'execute_token' => $this->esc($this->cms()->bundleExecuteToken()),
211 ), 'bundle') as $key => $value) {
212 $form->add_rep((string)$key, $value);
213 }
214 return $form->run();
215 }
216
224 public function handleImport(): string {
225 try {
226 $isBrowserUpload = $this->firstUploadFile() !== array() || isset($_POST['return_run1']);
227 if ($isBrowserUpload) {
228 $importForm = $this->importForm(
229 'ki-briefing-import-panel',
230 (string)($_POST['return_run1'] ?? '')
231 );
232 if (!$importForm->submit()) {
233 throw new \RuntimeException('Ungueltiger oder abgelaufener Formular-Token.');
234 }
235 }
236 $token = $this->newToken();
237 $payload = $this->readImportPayload($token);
238 $manifest = is_array($payload['manifest'] ?? null) ? $payload['manifest'] : array();
239 $context = is_array($payload['context'] ?? null) ? $payload['context'] : array();
240 $job = is_array($payload['job'] ?? null) ? $payload['job'] : array();
241 $assetsDir = (string)($payload['assets_dir'] ?? '');
242 $readme = (string)($payload['readme'] ?? '');
243
244 $validation = $this->validateJob($job, $assetsDir);
245 $preview = $this->buildPreview($job, $assetsDir, $manifest);
246
247 $state = array(
248 'area' => self::AREA,
249 'status' => 'preview_ready',
250 'percent' => 0,
251 'step_percent' => 0,
252 'message' => 'Bundle geprueft. Ausfuehrung starten.',
253 'manifest' => $manifest,
254 'context' => $context,
255 'job' => $job,
256 'assets_dir' => $assetsDir,
257 'readme' => $readme,
258 'validation' => $validation,
259 'preview' => $preview,
260 'step_pos' => 0,
261 'step_results' => array(),
262 'total' => count($job['steps'] ?? array()),
263 'title' => (string)($manifest['title'] ?? 'KI-Bundle'),
264 'recipe' => (string)($manifest['recipe'] ?? ''),
265 'lng' => (string)($manifest['lng'] ?? dbxContentLng::current()),
266 'return_run1' => $this->sanitizeReturnRun1((string)($_POST['return_run1'] ?? '')),
267 );
268 $this->setJob($token, $state);
269
270 if ($this->manifestAutoExecute($manifest)) {
271 try {
272 $this->authorizeAutoExecuteManifest();
273 $state['status'] = 'running';
274 $state['message'] = 'Bundle geprueft. Automatische Ausfuehrung gestartet.';
275 $this->setJob($token, $state);
276 $next = $this->moduleUrl('bundle_process', array('proc_key' => $token));
277 return $this->renderProcess($state, $next);
278 } catch (\Throwable $e) {
279 $state['status'] = 'error';
280 $state['message'] = $e->getMessage();
281 $this->setJob($token, $state);
282 $next = $this->moduleUrl('bundle_process', array('proc_key' => $token));
283 return $this->renderProcess($state, $next);
284 }
285 }
286
287 return $this->renderPreviewPage($token, $state);
288 } catch (\Throwable $e) {
289 dbx()->sys_msg('error', 'dbxKi', 'bundle_import', 'Import fehlgeschlagen', $e->getMessage());
290 $backRun1 = $this->sanitizeReturnRun1((string)($_POST['return_run1'] ?? ''));
291 $backUrl = $backRun1 !== '' ? $this->moduleUrl($backRun1) : $this->moduleUrl('briefing');
292 $tpl = dbx()->get_system_obj('dbxTPL');
293 return $tpl->get_tpl('dbxKi|ki-bundle-import-error', $this->withModuleBar(array(
294 'message' => $this->esc($e->getMessage()),
295 'back_url' => $this->esc($backUrl),
296 'briefing_url' => $this->esc($this->moduleUrl('briefing')),
297 ), 'bundle'));
298 }
299 }
300
301 private function readImportPayload(string $token): array {
302 $file = $this->firstUploadFile();
303 if ($file) {
304 return $this->extractZipUpload($file, $token);
305 }
306
307 $rawJob = trim((string)($_POST['job_json'] ?? ''));
308 if ($rawJob !== '') {
309 $job = json_decode($rawJob, true);
310 if (!is_array($job)) {
311 throw new \InvalidArgumentException('job_json ist kein gueltiges JSON.');
312 }
313 $manifest = array('title' => 'JSON-Job', 'lng' => dbxContentLng::current(), 'bundle_version' => self::BUNDLE_VERSION);
314 $rawManifest = trim((string)($_POST['manifest_json'] ?? ''));
315 if ($rawManifest !== '') {
316 $decoded = json_decode($rawManifest, true);
317 if (is_array($decoded)) {
318 $manifest = array_merge($manifest, $decoded);
319 }
320 }
321 return array(
322 'manifest' => $manifest,
323 'context' => array(),
324 'job' => $job,
325 'assets_dir' => '',
326 'readme' => '',
327 );
328 }
329
330 throw new \InvalidArgumentException('Bitte ZIP-Datei oder job.json senden.');
331 }
332
333 private function firstUploadFile(): array {
334 if (empty($_FILES) || !is_array($_FILES)) {
335 return array();
336 }
337 foreach ($_FILES as $file) {
338 if (!is_array($file)) {
339 continue;
340 }
341 if (isset($file['tmp_name']) && !is_array($file['tmp_name'])) {
342 return $file;
343 }
344 }
345 return array();
346 }
347
348 private function extractZipUpload(array $file, string $token): array {
349 if (($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
350 throw new \InvalidArgumentException('ZIP-Upload fehlgeschlagen.');
351 }
352 $tmp = (string)($file['tmp_name'] ?? '');
353 if ($tmp === '' || !is_uploaded_file($tmp)) {
354 throw new \InvalidArgumentException('ZIP-Upload ungueltig.');
355 }
356 if (!class_exists('ZipArchive')) {
357 throw new \RuntimeException('ZipArchive ist auf dem Server nicht verfuegbar.');
358 }
359
360 $maxBytes = $this->configInt('max_bundle_bytes', 52428800);
361 if ((int)($file['size'] ?? 0) > $maxBytes) {
362 throw new \InvalidArgumentException('ZIP ueberschreitet max_bundle_bytes.');
363 }
364
365 $token = $this->sanitizeToken($token);
366 if ($token === '') {
367 throw new \InvalidArgumentException('Interner Bundle-Token fehlt.');
368 }
369 $dest = $this->jobDir($token);
370 if (is_dir($dest)) {
371 $this->removeJobDir($token);
372 }
373 @mkdir($dest, 0777, true);
374
375 $zip = new \ZipArchive();
376 if ($zip->open($tmp) !== true) {
377 throw new \InvalidArgumentException('ZIP konnte nicht geoeffnet werden.');
378 }
379
380 $maxFiles = $this->configInt('max_bundle_files', 50);
381 $totalUncompressed = 0;
382 for ($i = 0; $i < $zip->numFiles; $i++) {
383 $name = (string)$zip->getNameIndex($i);
384 if ($name === '' || strpos($name, '..') !== false || $name[0] === '/') {
385 $zip->close();
386 throw new \InvalidArgumentException('Unzulaessiger ZIP-Pfad: ' . $name);
387 }
388 $stat = $zip->statIndex($i);
389 $totalUncompressed += (int)($stat['size'] ?? 0);
390 if ($totalUncompressed > $maxBytes) {
391 $zip->close();
392 throw new \InvalidArgumentException('Entpacktes Bundle zu gross.');
393 }
394 }
395 if ($zip->numFiles > $maxFiles) {
396 $zip->close();
397 throw new \InvalidArgumentException('Zu viele Dateien im Bundle.');
398 }
399
400 if (!$zip->extractTo($dest)) {
401 $zip->close();
402 throw new \RuntimeException('ZIP konnte nicht entpackt werden.');
403 }
404 $zip->close();
405
406 $root = $this->findBundleRoot($dest);
407 $manifest = $this->readJsonFile($root . '/manifest.json', false);
408 $context = $this->readJsonFile($root . '/context.json', false);
409 $job = $this->readJsonFile($root . '/job.json', true);
410 $readme = is_file($root . '/README.md') ? (string)file_get_contents($root . '/README.md') : '';
411 $assetsDir = is_dir($root . '/assets') ? $root . '/assets' : '';
412
413 return array(
414 'manifest' => is_array($manifest) ? $manifest : array(),
415 'context' => is_array($context) ? $context : array(),
416 'job' => $job,
417 'assets_dir' => $assetsDir,
418 'readme' => $readme,
419 );
420 }
421
422 private function findBundleRoot(string $dest): string {
423 if (is_file($dest . '/job.json')) {
424 return $dest;
425 }
426 foreach (glob($dest . '/*', GLOB_ONLYDIR) ?: array() as $dir) {
427 if (is_file($dir . '/job.json')) {
428 return $dir;
429 }
430 }
431 throw new \InvalidArgumentException('job.json im Bundle nicht gefunden.');
432 }
433
434 private function readJsonFile(string $path, bool $required): array {
435 if (!is_file($path)) {
436 if ($required) {
437 throw new \InvalidArgumentException('Pflichtdatei fehlt: ' . basename($path));
438 }
439 return array();
440 }
441 $raw = file_get_contents($path);
442 $data = json_decode((string)$raw, true);
443 if (!is_array($data)) {
444 throw new \InvalidArgumentException('Ungueltiges JSON: ' . basename($path));
445 }
446 return $data;
447 }
448
449 private function validateJob(array $job, string $assetsDir): array {
450 $steps = $job['steps'] ?? null;
451 if (!is_array($steps) || !$steps) {
452 throw new \InvalidArgumentException('job.json muss ein nicht-leeres steps-Array enthalten.');
453 }
454
455 $warnings = array();
456 $errors = array();
457 $seenIds = array();
458 $stepResults = array();
459 $cms = $this->cms();
460
461 foreach ($steps as $index => $step) {
462 if (!is_array($step)) {
463 $errors[] = 'Step ' . $index . ' ist kein Objekt.';
464 continue;
465 }
466 $id = trim((string)($step['id'] ?? ''));
467 if ($id === '') {
468 $errors[] = 'Step ' . $index . ' ohne id.';
469 continue;
470 }
471 if (isset($seenIds[$id])) {
472 $errors[] = 'Doppelte step id: ' . $id;
473 }
474 $seenIds[$id] = true;
475
476 $action = trim((string)($step['action'] ?? ''));
477 if ($action === '') {
478 $errors[] = 'Step ' . $id . ' ohne action.';
479 continue;
480 }
481 if (!$cms->bundleIsAllowedInPackage($action)) {
482 $errors[] = 'Step ' . $id . ': Aktion nicht erlaubt (' . $action . ').';
483 continue;
484 }
485
486 $params = is_array($step['params'] ?? null) ? $step['params'] : array();
487 try {
488 $this->validateRefTargets($params, array_keys($stepResults));
489 $resolved = $this->resolveParams($params, $stepResults, $assetsDir, false);
490 $cms->bundleBuildPlan($action, $resolved);
491 $stepResults[$id] = $this->syntheticStepResult($action);
492 } catch (\Throwable $e) {
493 if ($this->paramsContainRef($params) && $this->isPreviewRefDependencyError($e)) {
494 $stepResults[$id] = $this->syntheticStepResult($action);
495 continue;
496 }
497 $warnings[] = 'Step ' . $id . ' (' . $action . '): ' . $e->getMessage();
498 }
499 }
500
501 if ($errors) {
502 throw new \InvalidArgumentException(implode(' ', $errors));
503 }
504
505 return array(
506 'ok' => 1,
507 'step_count' => count($steps),
508 'warnings' => $warnings,
509 );
510 }
511
512 private function buildPreview(array $job, string $assetsDir, array $manifest): array {
513 $lines = array();
514 $title = trim((string)($manifest['title'] ?? ''));
515 if ($title !== '') {
516 $lines[] = 'Titel: ' . $title;
517 }
518 $recipe = trim((string)($manifest['recipe'] ?? ''));
519 if ($recipe !== '') {
520 $lines[] = 'Rezept: ' . $recipe;
521 }
522 $lng = trim((string)($manifest['lng'] ?? ''));
523 if ($lng !== '') {
524 $lines[] = 'Sprache: ' . $lng;
525 }
526
527 foreach ($job['steps'] ?? array() as $step) {
528 if (!is_array($step)) {
529 continue;
530 }
531 $lines[] = '- ' . ($step['id'] ?? '?') . ': ' . ($step['action'] ?? '?');
532 }
533
534 if ($assetsDir !== '' && is_dir($assetsDir)) {
535 $count = count(glob(rtrim($assetsDir, '/\\') . '/*') ?: array());
536 $lines[] = 'Assets: ' . $count . ' Datei(en)';
537 }
538
539 return array(
540 'lines' => $lines,
541 'step_count' => count($job['steps'] ?? array()),
542 );
543 }
544
545 private function renderPreviewPage(string $token, array $state): string {
546 $tpl = dbx()->get_system_obj('dbxTPL');
547 $warnings = is_array($state['validation']['warnings'] ?? null) ? $state['validation']['warnings'] : array();
548 $lines = is_array($state['preview']['lines'] ?? null) ? $state['preview']['lines'] : array();
549
550 $warningHtml = '<li class="text-muted">Keine</li>';
551 foreach ($warnings as $warning) {
552 $warningHtml = '';
553 break;
554 }
555 if ($warningHtml === '') {
556 foreach ($warnings as $warning) {
557 $warningHtml .= '<li>' . $this->esc($warning) . '</li>';
558 }
559 }
560
561 $lineHtml = '';
562 foreach ($lines as $line) {
563 $lineHtml .= '<li class="list-group-item py-1 px-2">' . $this->esc($line) . '</li>';
564 }
565
566 $warningBlock = '';
567 if ($warningHtml !== '<li class="text-muted">Keine</li>') {
568 $warningBlock = '<div class="small text-warning mb-2"><strong>Hinweise</strong><ul class="mb-0 ps-3">'
569 . $warningHtml . '</ul></div>';
570 }
571
572 $readme = trim((string)($state['readme'] ?? ''));
573 $readmeHtml = '';
574 if ($readme !== '') {
575 $readmeHtml = '<details class="small mt-2"><summary class="text-muted">README der KI-ZIP</summary>'
576 . '<pre class="small bg-light p-2 rounded mt-1 mb-0" style="max-height:8rem;overflow:auto">'
577 . $this->esc($readme) . '</pre></details>';
578 }
579
580 $backUrl = $this->returnUrlFromState($state);
581 $startUrl = $this->moduleUrl('bundle_process', array(
582 'proc_key' => $token,
583 'reset' => 1,
584 'proc_cmd' => 'start',
585 'token' => $this->cms()->bundleExecuteToken(),
586 ));
587 $footerActions = $this->buildImportFooterActions($state, true, $startUrl, $backUrl);
588 $barTitle = trim((string)($state['title'] ?? ''));
589
590 return $tpl->get_tpl('dbxKi|ki-bundle-preview', $this->withModuleBar(array(
591 'subtitle' => $this->esc((string)($state['recipe'] ?? '')),
592 'step_count' => (int)($state['total'] ?? 0),
593 'preview_list' => $lineHtml,
594 'warning_block' => $warningBlock,
595 'readme_block' => $readmeHtml,
596 'footer_actions' => $footerActions,
597 ), 'bundle_preview', '', $barTitle));
598 }
599
600 private function cmsAdminPageUrl(int $cid, string $lng = ''): string {
601 $url = '?dbx_modul=dbxContent_admin&dbx_run1=cms&cid=' . (int)$cid;
602 $lng = strtolower(trim($lng));
603 if ($lng !== '') {
604 $url .= '&dbx_lng=' . rawurlencode($lng);
605 }
606 return $url;
607 }
608
609 private function resolveContentPageRef(array $state): array {
610 $lng = strtolower(trim((string)($state['lng'] ?? '')));
611 $stepResults = is_array($state['step_results'] ?? null) ? $state['step_results'] : array();
612 foreach ($stepResults as $result) {
613 if (!is_array($result)) {
614 continue;
615 }
616 $cid = (int)($result['page_id'] ?? 0);
617 if ($cid > 0) {
618 $resultLng = strtolower(trim((string)($result['lng'] ?? '')));
619 return array(
620 'cid' => $cid,
621 'lng' => $resultLng !== '' ? $resultLng : $lng,
622 );
623 }
624 }
625
626 $job = is_array($state['job'] ?? null) ? $state['job'] : array();
627 foreach ($job['steps'] ?? array() as $step) {
628 if (!is_array($step)) {
629 continue;
630 }
631 $action = (string)($step['action'] ?? '');
632 $params = is_array($step['params'] ?? null) ? $step['params'] : array();
633 if ($action === 'page.update') {
634 $cid = (int)($params['id'] ?? 0);
635 if ($cid > 0) {
636 $stepLng = strtolower(trim((string)($params['lng'] ?? '')));
637 return array('cid' => $cid, 'lng' => $stepLng !== '' ? $stepLng : $lng);
638 }
639 }
640 }
641 return array('cid' => 0, 'lng' => $lng);
642 }
643
644 private function buildImportFooterActions(array $state, bool $showExecute, string $executeUrl, string $backUrl): string {
645 $html = '';
646 if ($showExecute && $executeUrl !== '') {
647 $html .= '<a class="btn btn-primary btn-sm" href="' . $this->esc($executeUrl)
648 . '"><i class="bi bi-play-fill"></i> Ausfuehren</a>';
649 }
650
651 $pageRef = $this->resolveContentPageRef($state);
652 if ((int)($pageRef['cid'] ?? 0) > 0) {
653 $html .= '<a class="btn btn-success btn-sm" href="' . $this->esc($this->cmsAdminPageUrl((int)$pageRef['cid'], (string)($pageRef['lng'] ?? '')))
654 . '"><i class="bi bi-pencil-square"></i> Seite im CMS</a>';
655 }
656
657 $html .= '<a class="btn btn-outline-primary btn-sm" href="' . $this->esc($this->moduleUrl('briefing'))
658 . '"><i class="bi bi-plus-lg"></i> Neuer KI-Auftrag</a>';
659 $html .= '<a class="btn btn-outline-secondary btn-sm" href="' . $this->esc($backUrl)
660 . '"><i class="bi bi-arrow-left"></i> Zurueck</a>';
661 return $html;
662 }
663
664 private function sanitizeReturnRun1(string $run1): string {
665 $allowed = array(
666 'briefing',
667 'briefing_page_create',
668 'briefing_page_update',
669 'briefing_page_translate',
670 'bundle',
671 );
672 $run1 = strtolower(trim($run1));
673 return in_array($run1, $allowed, true) ? $run1 : 'bundle';
674 }
675
676 private function returnUrlFromState(array $state): string {
677 $run1 = trim((string)($state['return_run1'] ?? ''));
678 if ($run1 === '') {
679 return $this->moduleUrl('bundle');
680 }
681 return $this->moduleUrl($this->sanitizeReturnRun1($run1));
682 }
683
684 public function handleProcess(): string {
685 $token = $this->sanitizeToken((string)($_GET['proc_key'] ?? ''));
686 if ($token === '') {
687 $token = $this->newToken();
688 }
689
690 $cmd = strtolower(preg_replace('/[^a-z_]+/', '', (string)($_GET['proc_cmd'] ?? '')));
691 $reset = (int)($_GET['reset'] ?? 0);
692 $state = $this->getJob($token);
693
694 if (!$state) {
695 return '<div class="container py-4"><div class="alert alert-warning">Bundle-Prozess nicht gefunden.</div>'
696 . '<p><a class="btn btn-secondary" href="' . $this->esc($this->moduleUrl('bundle')) . '">Zurueck</a></p></div>';
697 }
698
699 if ($reset || $cmd === 'restart') {
700 try {
701 if ($cmd === 'start' || $reset || $cmd === 'restart') {
702 $this->authorizeBundleExecute();
703 }
704 } catch (\Throwable $e) {
705 $state['status'] = 'error';
706 $state['message'] = $e->getMessage();
707 $this->setJob($token, $state);
708 $next = $this->moduleUrl('bundle_process', array('proc_key' => $token));
709 return $this->renderProcess($state, $next);
710 }
711 $state['status'] = 'running';
712 $state['step_pos'] = 0;
713 $state['step_results'] = array();
714 $state['percent'] = 0;
715 $state['step_percent'] = 0;
716 $state['message'] = 'Bundle-Ausfuehrung gestartet.';
717 } elseif ($cmd === 'start' && ($state['status'] ?? '') === 'preview_ready') {
718 try {
719 $this->authorizeBundleExecute();
720 } catch (\Throwable $e) {
721 $state['status'] = 'error';
722 $state['message'] = $e->getMessage();
723 $this->setJob($token, $state);
724 $next = $this->moduleUrl('bundle_process', array('proc_key' => $token));
725 return $this->renderProcess($state, $next);
726 }
727 $state['status'] = 'running';
728 $state['step_pos'] = 0;
729 $state['step_results'] = array();
730 $state['percent'] = 0;
731 $state['step_percent'] = 0;
732 $state['message'] = 'Bundle-Ausfuehrung gestartet.';
733 }
734
735 if ($cmd === 'pause' && ($state['status'] ?? '') === 'running') {
736 $state['status'] = 'paused';
737 $state['message'] = 'Bundle angehalten.';
738 } elseif ($cmd === 'resume' && ($state['status'] ?? '') === 'paused') {
739 $state['status'] = 'running';
740 $state['message'] = 'Bundle fortgesetzt.';
741 } elseif ($cmd === 'cancel') {
742 $state['status'] = 'canceled';
743 $state['message'] = 'Bundle abgebrochen.';
744 }
745
746 if (($state['status'] ?? '') === 'running') {
747 $this->tickJob($state);
748 }
749
750 $this->setJob($token, $state);
751 $next = $this->moduleUrl('bundle_process', array('proc_key' => $token));
752 return $this->renderProcess($state, $next);
753 }
754
755 private function authorizeBundleExecute(): void {
756 if ((int)dbx()->get_config('dbxKi', 'allow_execute', 1) !== 1) {
757 throw new \RuntimeException('Automatische Ausfuehrung ist deaktiviert (allow_execute).');
758 }
759 $token = trim((string)($_GET['token'] ?? $_POST['token'] ?? ''));
760 if (!$this->cms()->bundleCheckExecuteToken($token)) {
761 throw new \RuntimeException('Ungueltiges oder abgelaufenes Ausfuehrungs-Token.');
762 }
763 }
764
765 private function manifestAutoExecute(array $manifest): bool {
766 $value = $manifest['auto_execute'] ?? false;
767 if (is_bool($value)) {
768 return $value;
769 }
770 $value = strtolower(trim((string)$value));
771 return in_array($value, array('1', 'true', 'yes', 'ja', 'on'), true);
772 }
773
774 private function authorizeAutoExecuteManifest(): void {
775 if ((int)dbx()->get_config('dbxKi', 'allow_execute', 1) !== 1) {
776 throw new \RuntimeException('Automatische Ausfuehrung ist deaktiviert (allow_execute).');
777 }
778 }
779
780 private function tickJob(array &$state): void {
781 $steps = is_array($state['job']['steps'] ?? null) ? $state['job']['steps'] : array();
782 $total = count($steps);
783 $pos = (int)($state['step_pos'] ?? 0);
784 if ($total <= 0) {
785 $state['status'] = 'error';
786 $state['message'] = 'Keine Steps im Bundle.';
787 return;
788 }
789 if ($pos >= $total) {
790 $state['status'] = 'finished';
791 $state['percent'] = 100;
792 $state['step_percent'] = 100;
793 $state['message'] = $this->finishedMessage($state);
794 return;
795 }
796
797 $step = $steps[$pos];
798 $stepId = (string)($step['id'] ?? ('step_' . $pos));
799 $action = (string)($step['action'] ?? '');
800 $assetsDir = (string)($state['assets_dir'] ?? '');
801 $stepResults = is_array($state['step_results'] ?? null) ? $state['step_results'] : array();
802
803 try {
804 $params = is_array($step['params'] ?? null) ? $step['params'] : array();
805 $resolved = $this->resolveParams($params, $stepResults, $assetsDir, false);
806 $plan = $this->cms()->bundleBuildPlan($action, $resolved);
807 $result = $this->cms()->bundleExecutePlan($action, $resolved, $plan);
808 $stepResults[$stepId] = $this->normalizeStepResult($action, $result);
809 $state['step_results'] = $stepResults;
810 $state['step_pos'] = $pos + 1;
811 $state['message'] = 'Step ' . $stepId . ' (' . $action . ') ausgefuehrt.';
812 } catch (\Throwable $e) {
813 $state['status'] = 'error';
814 $state['message'] = 'Fehler in Step ' . $stepId . ': ' . $e->getMessage();
815 dbx()->sys_msg('error', 'dbxKi', 'bundle_process', $stepId, $e->getMessage());
816 return;
817 }
818
819 $done = (int)$state['step_pos'];
820 $state['percent'] = (int)floor(($done / $total) * 100);
821 $state['step_percent'] = 100;
822 if ($done >= $total) {
823 $state['status'] = 'finished';
824 $state['percent'] = 100;
825 $state['message'] = $this->finishedMessage($state);
826 }
827 }
828
829 private function finishedMessage(array $state): string {
830 $parts = array('Bundle abgeschlossen.');
831 foreach (is_array($state['step_results'] ?? null) ? $state['step_results'] : array() as $id => $result) {
832 if (!empty($result['page_id'])) {
833 $parts[] = 'Seite #' . (int)$result['page_id'];
834 }
835 if (!empty($result['id']) && empty($result['page_id']) && empty($result['media_id'])) {
836 $parts[] = $id . ' id=' . (int)$result['id'];
837 }
838 }
839 return implode(' ', $parts);
840 }
841
842 private function normalizeStepResult(string $action, array $result): array {
843 $out = $result;
844 if (!empty($result['id'])) {
845 $out['id'] = (int)$result['id'];
846 }
847 if (strpos($action, 'page.') === 0) {
848 $out['page_id'] = (int)($result['id'] ?? 0);
849 }
850 if (strpos($action, 'media.create') === 0) {
851 $out['media_id'] = (int)($result['id'] ?? 0);
852 }
853 if ($action === 'media.assign' && !empty($result['usage_id'])) {
854 $out['usage_id'] = (int)$result['usage_id'];
855 }
856 if ($action === 'translation.apply') {
857 $out['page_id'] = (int)($result['id'] ?? 0);
858 if (!empty($result['lng'])) {
859 $out['lng'] = (string)$result['lng'];
860 }
861 }
862 if (strpos($action, 'folder.') === 0 && !empty($result['id'])) {
863 $out['folder_id'] = (int)$result['id'];
864 }
865 return $out;
866 }
867
868 private function resolveParams($value, array $stepResults, string $assetsDir, bool $dryRun) {
869 if (is_array($value)) {
870 $out = array();
871 foreach ($value as $key => $item) {
872 $out[$key] = $this->resolveParams($item, $stepResults, $assetsDir, $dryRun);
873 }
874 if (isset($out['asset_ref']) && $assetsDir !== '') {
875 $out = $this->applyAssetRef($out, $assetsDir, $dryRun);
876 }
877 return $out;
878 }
879
880 if (!is_string($value)) {
881 return $value;
882 }
883
884 if (strpos($value, '$ref:') === 0) {
885 if ($dryRun) {
886 return 0;
887 }
888 return $this->resolveRef($value, $stepResults);
889 }
890
891 if (strpos($value, '$ref:') !== false) {
892 if ($dryRun) {
893 return $value;
894 }
895 return preg_replace_callback('/\$ref:([A-Za-z0-9_.-]+)/', function($match) use ($stepResults) {
896 return (string)$this->resolveRef('$ref:' . $match[1], $stepResults);
897 }, $value);
898 }
899
900 return $value;
901 }
902
903 private function paramsContainRef($value): bool {
904 if (is_array($value)) {
905 foreach ($value as $item) {
906 if ($this->paramsContainRef($item)) {
907 return true;
908 }
909 }
910 return false;
911 }
912 return is_string($value) && strpos($value, '$ref:') !== false;
913 }
914
915 private function validateRefTargets($value, array $completedStepIds): void {
916 if (is_array($value)) {
917 foreach ($value as $item) {
918 $this->validateRefTargets($item, $completedStepIds);
919 }
920 return;
921 }
922 if (!is_string($value) || strpos($value, '$ref:') !== 0) {
923 return;
924 }
925 $raw = substr($value, 5);
926 $parts = explode('.', $raw, 2);
927 if (count($parts) !== 2 || $parts[0] === '' || $parts[1] === '') {
928 throw new \InvalidArgumentException('Ungueltige Referenz: ' . $value);
929 }
930 if (!in_array($parts[0], $completedStepIds, true)) {
931 throw new \InvalidArgumentException('Referenz auf noch nicht ausgefuehrten Step: ' . $value);
932 }
933 }
934
935 private function isPreviewRefDependencyError(\Throwable $e): bool {
936 $msg = $e->getMessage();
937 return strpos($msg, 'größer als 0') !== false
938 || strpos($msg, 'groesser als 0') !== false
939 || strpos($msg, 'nicht gefunden') !== false;
940 }
941
942 private function syntheticStepResult(string $action): array {
943 if (strpos($action, 'media.create') === 0) {
944 return array('id' => 900001, 'media_id' => 900001);
945 }
946 if ($action === 'page.create' || $action === 'page.update') {
947 return array('id' => 900002, 'page_id' => 900002);
948 }
949 if ($action === 'media.assign') {
950 return array('usage_id' => 900003);
951 }
952 if (strpos($action, 'folder.create') === 0) {
953 return array('id' => 900004, 'folder_id' => 900004);
954 }
955 return array('id' => 900000);
956 }
957
958 private function resolveRef(string $value, array $stepResults) {
959 $raw = substr($value, 5);
960 $parts = explode('.', $raw, 2);
961 if (count($parts) !== 2) {
962 throw new \InvalidArgumentException('Ungueltige Referenz: ' . $value);
963 }
964 $stepId = $parts[0];
965 $field = $parts[1];
966 if (!isset($stepResults[$stepId]) || !is_array($stepResults[$stepId])) {
967 throw new \RuntimeException('Referenz nicht aufloesbar: ' . $value);
968 }
969 if (!array_key_exists($field, $stepResults[$stepId])) {
970 throw new \RuntimeException('Feld in Referenz fehlt: ' . $value);
971 }
972 return $stepResults[$stepId][$field];
973 }
974
975 private function applyAssetRef(array $params, string $assetsDir, bool $dryRun): array {
976 $ref = ltrim(str_replace('\\', '/', (string)($params['asset_ref'] ?? '')), '/');
977 if (strpos($ref, 'assets/') === 0) {
978 $ref = substr($ref, 7);
979 }
980 if ($ref === '' || strpos($ref, '..') !== false) {
981 throw new \InvalidArgumentException('Ungueltiger asset_ref.');
982 }
983 $path = rtrim(str_replace('\\', '/', $assetsDir), '/') . '/' . $ref;
984 $realAssets = realpath($assetsDir);
985 $realFile = realpath($path);
986 if (!$realAssets || !$realFile || strpos($realFile, $realAssets) !== 0 || !is_file($realFile)) {
987 throw new \InvalidArgumentException('Asset nicht gefunden: ' . $ref);
988 }
989 if ($dryRun) {
990 $params['data_base64'] = base64_encode('dry-run');
991 } else {
992 $bytes = file_get_contents($realFile);
993 if ($bytes === false) {
994 throw new \RuntimeException('Asset konnte nicht gelesen werden: ' . $ref);
995 }
996 $params['data_base64'] = base64_encode($bytes);
997 }
998 unset($params['asset_ref']);
999 if (empty($params['file_name'])) {
1000 $params['file_name'] = basename($realFile);
1001 }
1002 return $params;
1003 }
1004
1005 private function renderProcess(array $state, string $nextUrl): string {
1006 $tpl = dbx()->get_system_obj('dbxTPL');
1007 $status = strtolower((string)($state['status'] ?? 'running'));
1008 $percent = max(0, min(100, (int)($state['percent'] ?? 0)));
1009 $stepPercent = max(0, min(100, (int)($state['step_percent'] ?? 0)));
1010 $token = (string)($state['proc_key'] ?? '');
1011
1012 $statusLabels = array(
1013 'preview_ready' => 'Bereit',
1014 'running' => 'Laeuft',
1015 'paused' => 'Pausiert',
1016 'finished' => 'Fertig',
1017 'error' => 'Fehler',
1018 'canceled' => 'Abgebrochen',
1019 );
1020 $statusClasses = array(
1021 'running' => 'text-bg-primary',
1022 'paused' => 'text-bg-warning',
1023 'finished' => 'text-bg-success',
1024 'error' => 'text-bg-danger',
1025 'canceled' => 'text-bg-secondary',
1026 'preview_ready' => 'text-bg-info',
1027 );
1028 $statusIcons = array(
1029 'running' => 'bi bi-arrow-repeat',
1030 'paused' => 'bi bi-pause-fill',
1031 'finished' => 'bi bi-check-lg',
1032 'error' => 'bi bi-exclamation-triangle',
1033 'canceled' => 'bi bi-x-lg',
1034 'preview_ready' => 'bi bi-hourglass',
1035 );
1036
1037 $append = function(string $url, array $params): string {
1038 foreach ($params as $key => $value) {
1039 if ($value === null || $value === '') {
1040 continue;
1041 }
1042 $url .= (strpos($url, '?') === false ? '?' : '&')
1043 . rawurlencode((string)$key) . '=' . rawurlencode((string)$value);
1044 }
1045 return $url;
1046 };
1047
1048 $targetId = 'dbx_ki_bundle_' . substr(md5($token), 0, 12);
1049 $autostart = ($status === 'running') ? 1 : 0;
1050 $tokenParam = $this->cms()->bundleExecuteToken();
1051 $backUrl = $this->returnUrlFromState($state);
1052 $finishedActions = '';
1053 if ($status === 'finished') {
1054 $finishedActions = $this->buildImportFooterActions($state, false, '', $backUrl);
1055 }
1056
1057 $barTitle = trim((string)($state['title'] ?? ''));
1058
1059 return $tpl->get_tpl('dbxKi|ki-bundle-process', $this->withModuleBar(array(
1060 'target_id' => $this->esc($targetId),
1061 'status_key' => $this->esc($status),
1062 'status_label' => $this->esc($statusLabels[$status] ?? $status),
1063 'status_class' => $this->esc($statusClasses[$status] ?? 'text-bg-secondary'),
1064 'status_icon' => $this->esc($statusIcons[$status] ?? 'bi bi-circle'),
1065 'message' => $this->esc((string)($state['message'] ?? '')),
1066 'percent' => $percent,
1067 'step_percent' => $stepPercent,
1068 'step_label' => $this->esc((int)($state['step_pos'] ?? 0) . ' / ' . (int)($state['total'] ?? 0)),
1069 'updated_at' => $this->esc((string)($state['updated_at'] ?? '')),
1070 'next_url' => $this->esc($nextUrl),
1071 'pause_url' => $this->esc($append($nextUrl, array('proc_cmd' => 'pause', 'token' => $tokenParam))),
1072 'resume_url' => $this->esc($append($nextUrl, array('proc_cmd' => 'resume', 'token' => $tokenParam))),
1073 'cancel_url' => $this->esc($append($nextUrl, array('proc_cmd' => 'cancel', 'token' => $tokenParam))),
1074 'restart_url' => $this->esc($append($nextUrl, array('reset' => 1, 'proc_cmd' => 'restart', 'token' => $tokenParam))),
1075 'back_url' => $this->esc($backUrl),
1076 'autostart' => $autostart,
1077 'interval' => 900,
1078 'pause_visible' => 'running',
1079 'resume_visible' => 'paused',
1080 'restart_visible' => 'paused,canceled,error,finished',
1081 'cancel_visible' => 'running,paused',
1082 'result_html' => $this->renderResultHtml($state),
1083 'finished_actions' => $finishedActions,
1084 ), 'bundle_process', '', $barTitle));
1085 }
1086
1087 private function renderResultHtml(array $state): string {
1088 if (($state['status'] ?? '') !== 'finished') {
1089 return '';
1090 }
1091 $items = '';
1092 foreach (is_array($state['step_results'] ?? null) ? $state['step_results'] : array() as $id => $result) {
1093 if (!is_array($result)) {
1094 continue;
1095 }
1096 $bits = array($this->esc($id));
1097 foreach (array('page_id', 'media_id', 'folder_id', 'usage_id', 'id') as $key) {
1098 if (!empty($result[$key])) {
1099 $bits[] = $key . '=' . (int)$result[$key];
1100 }
1101 }
1102 $items .= '<li class="list-group-item py-1 px-2 small">' . implode(' ', $bits) . '</li>';
1103 }
1104 if ($items === '') {
1105 return '';
1106 }
1107 return '<ul class="list-group list-group-flush small mb-0">' . $items . '</ul>';
1108 }
1109
1110 public function handleExport(): void {
1111 if (!class_exists('ZipArchive')) {
1112 dbx()->json_response(array('ok' => 0, 'error' => 'ZipArchive nicht verfuegbar.'), true);
1113 }
1114
1115 $cms = $this->cms();
1116 $lng = dbxContentLng::current();
1117 $describe = $this->describeBundle();
1118 $snapshot = $cms->bundleSnapshot(array('lng' => $lng, 'limit' => 50));
1119 $instructionsPath = dirname(__DIR__) . '/KI-INSTRUCTIONS.md';
1120 $instructions = is_file($instructionsPath) ? file_get_contents($instructionsPath) : '';
1121
1122 $manifest = array(
1123 'bundle_version' => self::BUNDLE_VERSION,
1124 'api_version' => '0.1',
1125 'title' => 'dbxKi CMS Briefing',
1126 'recipe' => 'page.create.v1',
1127 'lng' => $lng,
1128 'intent' => 'create',
1129 'area' => self::AREA,
1130 'auto_execute' => true,
1131 );
1132
1133 $exampleJob = array(
1134 'steps' => array(
1135 array(
1136 'id' => 'hero',
1137 'action' => 'media.create_base64',
1138 'params' => array(
1139 'file_name' => 'hero.jpg',
1140 'asset_ref' => 'hero.jpg',
1141 'title' => 'Hero',
1142 'alt' => 'Hero-Bild',
1143 ),
1144 ),
1145 array(
1146 'id' => 'page',
1147 'action' => 'page.create',
1148 'params' => array(
1149 'lng' => $lng,
1150 'folder_id' => 1,
1151 'title' => 'Neue KI-Seite',
1152 'template' => 'c-title-hero_header-body1-footer',
1153 'content' => '<p>Inhalt der Seite</p>',
1154 ),
1155 ),
1156 array(
1157 'id' => 'hero_assign',
1158 'action' => 'media.assign',
1159 'params' => array(
1160 'media_id' => '$ref:hero.media_id',
1161 'content_id' => '$ref:page.page_id',
1162 'slot' => 'hero',
1163 'lng' => $lng,
1164 ),
1165 ),
1166 ),
1167 );
1168
1169 $context = array(
1170 'lng' => $lng,
1171 'snapshot' => $snapshot,
1172 'note' => 'Ordner- und Seiten-IDs aus snapshot fuer folder_id / page.update verwenden.',
1173 );
1174
1175 $readme = "# dbxKi CMS Bundle\n\n"
1176 . "1. job.json und optionale assets/ bearbeiten\n"
1177 . "2. ZIP mit manifest.json, job.json, README.md erstellen\n"
1178 . "3. In dbxKi unter Bundle importieren\n";
1179
1180 $tmp = tempnam(sys_get_temp_dir(), 'dbxki');
1181 $zip = new \ZipArchive();
1182 if ($zip->open($tmp, \ZipArchive::OVERWRITE) !== true) {
1183 @unlink($tmp);
1184 throw new \RuntimeException('Export-ZIP konnte nicht erstellt werden.');
1185 }
1186
1187 $zip->addFromString('manifest.json', json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
1188 $zip->addFromString('job.json', json_encode($exampleJob, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
1189 $zip->addFromString('context.json', json_encode($context, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
1190 $zip->addFromString('bundle.describe.json', json_encode($describe, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
1191 $zip->addFromString('README.md', $readme);
1192 if ($instructions !== '') {
1193 $zip->addFromString('KI-INSTRUCTIONS.md', $instructions);
1194 }
1195 $zip->close();
1196
1197 $name = 'dbxki-cms-briefing-' . $lng . '.zip';
1198 header('Content-Type: application/zip');
1199 header('Content-Disposition: attachment; filename="' . $name . '"');
1200 header('Content-Length: ' . filesize($tmp));
1201 readfile($tmp);
1202 @unlink($tmp);
1203 exit;
1204 }
1205
1206 public function handleDescribeJson(): void {
1207 dbx()->json_response($this->describeBundle(), true);
1208 }
1209}
1210
1211?>
renderStartPage()
Rendert die Bundle-Startseite mit einem vollwertigen dbxForm-Upload.
handleImport()
Liest und validiert einen Bundle-Import.
renderImportPanel(string $returnRun1)
Rendert den kompakten Importblock unter einem KI-Briefing.
esc($value)
Escaped einen Wert fuer HTML-Text und HTML-Attribute.
Definition dbxApi.php:2310
get_file_dir()
Liefert das files/-Verzeichnis der Installation.
Definition dbxApi.php:1524
json_response(array $data, bool $withRuntime=false)
Beendet den Request mit JSON-Ausgabe.
Definition dbxApi.php:1647
if($stored !=='files/test/') $resolved
if(($result['name'] ?? '') !=='customer-design'||!is_dir($target)) if(substr_count( $defaultHtml, '[dbx:content]') !==1) foreach(array('[dbx:logo]', '[dbx:branding]', '[dbx:footer]') as $slot) if(( $metadata[ 'contract'] ?? '') !==\dbx\dbxDesign_admin\dbxDesignService::CONTRACT) if((dbx() ->config['dbx']['default_design_user'] ?? '') !=='customer-design') $validation
if(preg_match('/core\.js\? foreach[^"\']*v=(\d+)/', $designTemplate, $assetMatch) !== 1 || !str_contains($shopReference, 'dbxapp-Asset-Version ' . $assetMatch[1])) foreach (array( 'reference\\archive', 'provision_docs_content.php', 'dbxSelfTest') as $needle)(array('Installation'=> $installation, 'Installations-Tutorial'=> $installationTutorial, 'SelfTest'=> $selfTest) as $label=> $html)
if(strpos($template, 'Module bearbeiten und aktualisieren')===false||strpos($template, 'Handbuch+Referenz')===false) $instructions
if($web->content_permalink_redirect_target() !=='') $tpl
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.