dbxapp 4.1.3
CMS, Shop, Workflows und modulare Geschäftsanwendungen
Loading...
Searching...
No Matches
dbxKiModuleBriefingService.class.php
Go to the documentation of this file.
1<?php
2namespace dbx\dbxKi;
3
5
6 private const MODULE_BRIEFING_VERSION = '0.6';
7
8 private function esc($value): string {
9 return htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8');
10 }
11
12 private function modulesRoot(): string {
13 return dbx()->os_path(dbx()->get_base_dir() . 'dbx/modules/');
14 }
15
16 private function validName($name): bool {
17 return is_string($name) && preg_match('/^[A-Za-z][A-Za-z0-9_]{1,63}$/', $name);
18 }
19
20 private function moduleDir(string $modul): string {
21 return dbx()->os_path($this->modulesRoot() . $modul . DIRECTORY_SEPARATOR);
22 }
23
24 private function modulePath(string $modul, string $rel): string {
25 $rel = str_replace(array('\\', "\0"), array('/', ''), $rel);
26 $rel = ltrim($rel, '/');
27 if ($rel === '' || strpos($rel, '../') !== false) {
28 return '';
29 }
30 $base = $this->moduleDir($modul);
31 $path = dbx()->os_path($base . $rel);
32 $baseNorm = str_replace('\\', '/', rtrim($base, '/\\') . '/');
33 $pathNorm = str_replace('\\', '/', $path);
34 return strpos($pathNorm, $baseNorm) === 0 ? $path : '';
35 }
36
37 private function moduleUrl(string $run1, array $params = array()): string {
38 $url = '?dbx_modul=dbxKi&dbx_run1=' . rawurlencode($run1);
39 foreach ($params as $key => $value) {
40 $url .= '&' . rawurlencode((string)$key) . '=' . rawurlencode((string)$value);
41 }
42 return $url;
43 }
44
59 private function managedForm(string $fid, string $template, string $action, array $replacements = array()) {
60 $form = dbx()->get_system_obj('dbxForm');
61 $form->init($fid, $template);
62 $form->_action = $action;
63 $form->_msg_info = '';
64 foreach ($replacements as $key => $value) {
65 $form->add_rep((string)$key, $value);
66 }
67 return $form;
68 }
69
70 private function selectOptions(array $options, string $selected): string {
71 $html = '';
72 foreach ($options as $value => $label) {
73 $sel = (string)$value === $selected ? ' selected' : '';
74 $html .= '<option value="' . $this->esc($value) . '"' . $sel . '>' . $this->esc($label) . '</option>';
75 }
76 return $html;
77 }
78
79 private function moduleOptions(string $selected = ''): string {
80 $html = '<option value="">Bitte Modul waehlen</option>';
81 $dirs = glob($this->modulesRoot() . '*', GLOB_ONLYDIR) ?: array();
82 sort($dirs);
83 foreach ($dirs as $dir) {
84 $name = basename($dir);
85 if (!$this->validName($name)) {
86 continue;
87 }
88 $sel = $name === $selected ? ' selected' : '';
89 $html .= '<option value="' . $this->esc($name) . '"' . $sel . '>' . $this->esc($name) . '</option>';
90 }
91 return $html;
92 }
93
94 private function fileTree(string $modul, bool $withContent = false): array {
95 $dir = $this->moduleDir($modul);
96 $rows = array();
97 if (!is_dir($dir)) {
98 return $rows;
99 }
100 $it = new \RecursiveIteratorIterator(
101 new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS),
102 \RecursiveIteratorIterator::SELF_FIRST
103 );
104 foreach ($it as $item) {
105 $full = $item->getPathname();
106 $rel = str_replace('\\', '/', substr($full, strlen(rtrim($dir, '/\\')) + 1));
107 $row = array(
108 'path' => 'dbx/modules/' . $modul . '/' . $rel,
109 'type' => $item->isDir() ? 'dir' : 'file',
110 'size' => $item->isFile() ? (int)$item->getSize() : 0,
111 );
112 if ($withContent && $item->isFile() && $this->isTextFile($full) && $item->getSize() <= 262144) {
113 $row['content'] = (string)file_get_contents($full);
114 }
115 $rows[] = $row;
116 }
117 return $rows;
118 }
119
120 private function isTextFile(string $file): bool {
121 return (bool)preg_match('/\.(php|phtml|pht|htm|html|css|js|json|md|txt|sql|xml|svg)$/i', $file);
122 }
123
124 private function moduleDescribe(string $modul = ''): array {
125 return array(
126 'ok' => 1,
127 'area' => 'module',
128 'module' => $modul,
129 'briefing_version' => self::MODULE_BRIEFING_VERSION,
130 'endpoint' => $this->moduleUrl('module_api'),
131 'hard_rules' => $this->hardRules($modul ?: '{module}'),
132 'dbxapp_way' => $this->dbxappWay(),
133 'reference_standard' => $this->referenceStandard(),
134 'module_pipeline' => $this->modulePipeline($modul ?: '{module}'),
135 'ui_contract' => $this->uiContract(),
136 'dbx_api_contract' => $this->dbxApiContract($modul ?: '{module}'),
137 'api_actions' => $this->apiCatalog(),
138 'answer_zip_contract' => $this->answerZipContract($modul ?: '{module}'),
139 );
140 }
141
142 private function hardRules(string $modul): array {
143 return array(
144 'Nur Dateien unter dbx/modules/' . $modul . '/ bearbeiten.',
145 'Keine Aenderungen an dbx/include, globaler config.php, anderen Modulen oder files/ ausser explizit erlaubten Modul-Assets.',
146 'Vor destruktiven Aenderungen muss ein vollstaendiges Modul-ZIP als Backup existieren.',
147 'Fachliche Datenbankzugriffe ausschliesslich ueber dbxDB und DD-Namen; kein PDO, mysqli, rohes SQL oder direkter Zugriff auf db3-Dateien.',
148 'create_date, create_uid, update_date, update_uid und owner werden von dbxDB gesetzt und duerfen im Modulcode nicht manuell geschrieben werden.',
149 'DD-Dateien muessen im dbxapp-Exportformat vollstaendig und direkt lesbar sein: TABLE, FIELDS und INDEXES explizit mit $table[...], $field[...] und $index[...] definieren. Keine $addField-Closure, keine DD-Includes und keine versteckende Hilfsabstraktion.',
150 'Produktive Tabellen-DDs verwenden trace=0, sofern keine ausdruecklich dokumentierte Systemausnahme besteht.',
151 'DD->DB Sync nur fuer ' . $modul . '|{dd}. Keine Migrationen, keine Altlasten.',
152 'Templates ueber dbxTPL lesen/rendern: dbx()->get_system_obj("dbxTPL")->get_tpl("' . $modul . '|template", $data).',
153 'Formulare ueber dbxForm bauen. Nach einem Insert die von dbxDB gelieferte RID in Formular und Action uebernehmen, damit weiteres Speichern ein Update ausfuehrt. Modulcode fuegt normalen Form-Actions keinen dbx_token hinzu; dbxForm signiert nur automatisch erkannte delete/save-RID-Actions.',
154 'Reports ueber dbxReport bauen; berechnete Spalten und Summen ueber den {fid}_next_record-Default umsetzen, Footerwerte spaet per add_rep() setzen und {rpt:colspan} nutzen. Keine reine str_replace()-Footermethode und keine unnoetigen Callback-Setter. Mutierende Standardaktionen werden automatisch signiert. Schreibende Grid-Routen verwenden *_grid_save, *_grid_insert, *_grid_delete, *_grid_sort oder *_grid_sync; Read bleibt tokenlos.',
155 'Keine Modulmethoden anlegen, die lediglich einen dbx()- oder get_system_obj()-Aufruf weiterreichen.',
156 'Nicht pauschal escapen. Nur an einer echten Ausgabe- oder Syntaxgrenze passend zum Zielkontext behandeln.',
157 'GET bleibt fuer Navigation und reine Anzeige erhalten. delete und save werden in dbx_run1/dbx_run2/dbx_run3/dbx_do zusammen mit rid automatisch erkannt; den Link nur mit dbx()->action_url($url) signieren. Keine action_routes-Konfiguration fuer diese Standardfaelle, keine manuellen Scopes und keine check_action_token()-Pruefung im Modulservice.',
158 'Ajax und normaler POST muessen denselben dbxForm-Ablauf verwenden und beide getestet werden.',
159 'Bestehende Tests erweitern oder einen fokussierten Vertrags-/Integrationstest im Modul ergaenzen.',
160 'Modul-README und Doxygen-Kommentare aktualisieren, wenn Verhalten, DD, Formular, Report oder API geaendert werden.',
161 'Das mitgelieferte myInvoices-Modul ist die ausfuehrbare Architektur-Referenz; Zielmodul und seine bestehenden Konventionen bleiben fuer Fachverhalten massgeblich.',
162 );
163 }
164
165 private function dbxappWay(): array {
166 return array(
167 'module_entry' => 'dbx/modules/{module}/{module}.class.php routet dbx_run1 und delegiert an include Services.',
168 'service_code' => 'Fachlogik liegt unter include/*.class.php im Modul.',
169 'dd' => 'Data Dictionary liegt unter dd/*.dd.php und beschreibt TABLE, FIELDS und INDEXES explizit im dbxapp-Exportformat. Jedes Feld wird direkt ueber $field[...] und $fields[]=$field definiert.',
170 'fd' => 'Form-/Report-Felder liegen unter fd/*.fd.php.',
171 'templates' => 'Templates liegen unter tpl/htm und werden per dbxTPL get_tpl gerendert.',
172 'sqlite' => '*.db3 fuer ein Modul bleibt im Modulkontext; DD->DB Sync ueber dbxAdmin Schema.',
173 'database' => 'Fachliche Abfragen und Aenderungen laufen ausschliesslich ueber dbxDB mit DD; Auditfelder setzt dbxDB automatisch.',
174 'create_update' => 'dbxForm speichert neue Datensaetze als Insert und arbeitet danach mit der gelieferten RID weiter, sodass derselbe Dialog Updates ausfuehrt.',
175 'form_security' => 'Normale POST-Form-Routen verwenden den rotierenden dbxForm-Submit-Schutz. Modulcode fuegt keinen dbx_token hinzu; nur eine als delete/save plus rid erkannte Action wird zentral signiert.',
176 'navigation' => 'GET fuer Navigation/Anzeige beibehalten; delete/save plus rid werden von action_url automatisch als mutierende Link-Aktion erkannt.',
177 'action_security' => 'dbxWebApp bindet die RID und prueft automatisch erkannte Routen vor dem Modulstart; Fachservices bauen oder pruefen keine eigenen Link-Token-Scopes.',
178 'grid_security' => 'dbxReport signiert Grid-Save, -Insert, -Delete, -Sort und -Sync anhand der eigentlichen *_grid_<aktion>-Route. Unbekannte Schreibkonventionen werden nicht unsigniert ausgegeben.',
179 'callbacks' => 'dbxForm uebernimmt den direkten Aufrufer als Owner; dbxReport nutzt {fid}_{event}-Defaults. Summen im next_record-Callback akkumulieren, spaet per add_rep() setzen und {rpt:colspan} verwenden.',
180 'system_objects' => 'dbx-Systemobjekte am Verwendungsort holen; keine privaten Methoden, die nur dbx()-Aufrufe kapseln.',
181 'reference' => 'reference/myInvoices und reference/25_Verbindliches_Modulhandbuch.md zeigen den verbindlichen Standard ausfuehrbar.',
182 );
183 }
184
185 private function referenceStandard(): array {
186 return array(
187 'manual_source' => '25_Verbindliches_Modulhandbuch.md',
188 'reference_module_source' => 'dbx/modules/myInvoices',
189 'export_manual' => 'reference/25_Verbindliches_Modulhandbuch.md',
190 'export_module' => 'reference/myInvoices',
191 'purpose' => 'Ausfuehrbare Referenz fuer dbxTPL, dbxDB, dbxForm, DD, FD, dbxReport, Callbacks, Ajax, Confirm, zentrale Action-Policies und Tests.',
192 'priority' => 'Die Referenz bestimmt Architektur und Vorgehen; vorhandenes Fachverhalten des Zielmoduls darf nicht unbeabsichtigt veraendert werden.',
193 );
194 }
195
196 private function apiCatalog(): array {
197 return array(
198 'system.describe' => array('method' => 'GET/POST', 'params' => array(), 'result' => 'Regeln, Aktionen, Antwort-ZIP-Vertrag'),
199 'module.describe' => array('params' => array('xmodul'), 'result' => 'Regeln und Kontext fuer ein Modul'),
200 'module.snapshot' => array('params' => array('xmodul', 'with_content=0|1'), 'result' => 'Dateibaum, optional kleine Textdateien mit Inhalt'),
201 'module.file.read' => array('params' => array('xmodul', 'path'), 'result' => 'Einzelne Textdatei innerhalb des Moduls'),
202 'module.pipeline_guide' => array('params' => array('xmodul', 'task_type'), 'result' => 'Verbindliches Job-Schema fuer Modul-Aenderungen'),
203 'module.job.execute' => array('method' => 'POST JSON', 'params' => array('manifest', 'job'), 'result' => 'Fuehrt ein Modul-job.json direkt ueber dieselbe Pipeline aus'),
204 );
205 }
206
207 private function answerZipContract(string $modul): array {
208 return array(
209 'filename' => 'antwort.zip',
210 'required_files' => array('manifest.json', 'job.json', 'README.md'),
211 'allowed_payload_paths' => array('dbx/modules/' . $modul . '
286 public function renderBriefing(): string {
287 $selected = dbx()->get_modul_var('xmodul', '', 'parameter');
288 $dd = dbx()->get_modul_var('dd_name', '', 'parameter');
289 $taskType = dbx()->get_modul_var('task_type', 'update', 'parameter');
290 $includeContext = dbx()->get_modul_var('include_context', 'full', 'parameter');
291 $export = $this->moduleUrl('briefing_module_export');
292 $api = $this->moduleUrl('module_api', array('action' => 'system.describe'));
293 $bundle = $this->moduleUrl('module_bundle');
294 $taskOptions = $this->selectOptions(array(
295 'update' => 'Bestehendes Modul bearbeiten / aktualisieren',
296 'extend' => 'Bestehendes Modul erweitern',
297 'repair' => 'Fehler im Modul reparieren',
298 'form_report' => 'Formular/Report erweitern',
299 'api' => 'Modul-API erweitern',
300 'refactor' => 'Intern aufraeumen',
301 ), $taskType);
302 $contextOptions = $this->selectOptions(array(
303 'full' => 'Komplettes Modul ins ZIP',
304 'tree' => 'Nur Dateibaum + Regeln',
305 ), $includeContext);
306
307 $actions = '<a class="btn btn-success btn-sm" href="' . $this->esc($bundle) . '"><i class="bi bi-upload"></i> Antwort importieren</a>'
308 . '<a class="btn btn-outline-secondary btn-sm" target="_blank" href="' . $this->esc($api) . '"><i class="bi bi-braces"></i> API</a>';
309 $help = dbx()->get_include_obj('dbxKiHelp', 'dbxKi');
310 $data = array_merge(array(
311 'export_url' => $this->esc($export),
312 'bundle_url' => $this->esc($bundle),
313 'api_url' => $this->esc($api),
314 'module_options' => $this->moduleOptions($selected),
315 'dd_name' => $this->esc($dd),
316 'task_options' => $taskOptions,
317 'context_options' => $contextOptions,
318 ), $help->moduleBarTemplateData('briefing_module', $actions));
319
320 return $this->managedForm(
321 'ki-module-briefing',
322 'ki-module-briefing',
323 $export,
324 $data
325 )->run();
326 }
327
328 public function renderBundleStart(): string {
329 $back = $this->moduleUrl('briefing_module');
330 $action = $this->moduleUrl('module_bundle_import');
331 $barActions = '<a class="btn btn-outline-secondary btn-sm" href="' . $this->esc($back) . '"><i class="bi bi-arrow-left"></i> Modul-KI</a>';
332 $help = dbx()->get_include_obj('dbxKiHelp', 'dbxKi');
333 $data = array_merge(array(
334 'import_url' => $this->esc($action),
335 'back_url' => $this->esc($back),
336 ), $help->moduleBarTemplateData('module_bundle', $barActions));
337
338 return $this->managedForm(
339 'ki-module-bundle-import',
340 'ki-module-bundle-import',
341 $action,
342 $data
343 )->run();
344 }
345
346 public function handleExport(): void {
347 $form = $this->managedForm(
348 'ki-module-briefing',
349 'ki-module-briefing',
350 $this->moduleUrl('briefing_module_export')
351 );
352 if (!$form->submit()) {
353 dbx()->json_response(array('ok' => 0, 'error' => 'Ungueltiger oder abgelaufener Formular-Token.'), true);
354 }
355
356 $modul = dbx()->get_request_var('xmodul', '', 'parameter');
357 if (!$this->validName($modul) || !is_dir($this->moduleDir($modul))) {
358 dbx()->json_response(array('ok' => 0, 'error' => 'Ungueltiges oder fehlendes Modul'), true);
359 }
360 $dd = dbx()->get_request_var('dd_name', '', 'parameter');
361 $brief = dbx()->get_request_var('brief', '', '*');
362 $taskType = dbx()->get_request_var('task_type', 'update', 'parameter');
363 $includeContext = dbx()->get_request_var('include_context', 'full', 'parameter');
364
365 $manifest = $this->moduleDescribe($modul);
366 $briefing = array(
367 'briefing_version' => self::MODULE_BRIEFING_VERSION,
368 'area' => 'module',
369 'module' => $modul,
370 'dd' => $dd,
371 'task_type' => $taskType,
372 'brief' => $brief,
373 'rules' => $this->hardRules($modul),
374 'pipeline' => $this->modulePipeline($modul),
375 'ui_contract' => $this->uiContract(),
376 'dbx_api_contract' => $this->dbxApiContract($modul),
377 'reference_standard' => $this->referenceStandard(),
378 );
379 $job = array(
380 'job_version' => self::MODULE_BRIEFING_VERSION,
381 'area' => 'module',
382 'module' => $modul,
383 'steps' => array(
384 array('id' => 'backup', 'action' => 'module.backup', 'params' => array('module' => $modul)),
385 array('id' => 'change_1', 'action' => 'module.file.write', 'params' => array('module' => $modul, 'path' => 'dbx/modules/' . $modul . '/___PFAD___', 'content' => '___KI_FUELLEN___')),
386 ),
387 );
388 $answerManifest = array(
389 'bundle_version' => self::MODULE_BRIEFING_VERSION,
390 'area' => 'module',
391 'module' => $modul,
392 'recipe' => 'module.update.v1',
393 'task_type' => $taskType,
394 'auto_execute' => true,
395 );
396
397 $files = array(
398 '00-START.md' => $this->startText($modul),
399 'KI-AUFTRAG.md' => $this->auftragText($modul, $dd, $brief),
400 'briefing.json' => json_encode($briefing, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
401 'job.vorlage.json' => json_encode($job, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
402 'manifest.vorlage.json' => json_encode($answerManifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
403 'module.describe.json' => json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
404 'module.snapshot.json' => json_encode(array('module' => $modul, 'files' => $this->fileTree($modul, false)), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
405 'README.md' => 'dbxKi Modul-Auftrag fuer ' . $modul . "\n",
406 'reference/README.md' => $this->referenceText(),
407 );
408
409 $tmp = tempnam(sys_get_temp_dir(), 'dbxkimod');
410 $zip = new \ZipArchive();
411 if ($zip->open($tmp, \ZipArchive::OVERWRITE) !== true) {
412 dbx()->json_response(array('ok' => 0, 'error' => 'ZIP konnte nicht erstellt werden'), true);
413 }
414 foreach ($files as $path => $content) {
415 $zip->addFromString($path, (string)$content);
416 }
417 if ($includeContext === 'full') {
418 $this->addModuleToZip($zip, $modul);
419 }
420 $this->addReferenceToZip($zip);
421 $zip->close();
422 $name = 'dbxki-modul-' . preg_replace('/[^A-Za-z0-9_-]+/', '-', $modul) . '.zip';
423 header('Content-Type: application/zip');
424 header('Content-Disposition: attachment; filename="' . $name . '"');
425 header('Content-Length: ' . filesize($tmp));
426 readfile($tmp);
427 @unlink($tmp);
428 exit;
429 }
430
431 private function addModuleToZip(\ZipArchive $zip, string $modul): void {
432 $dir = $this->moduleDir($modul);
433 $it = new \RecursiveIteratorIterator(
434 new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS),
435 \RecursiveIteratorIterator::SELF_FIRST
436 );
437 foreach ($it as $item) {
438 $full = $item->getPathname();
439 $rel = str_replace('\\', '/', substr($full, strlen(rtrim($dir, '/\\')) + 1));
440 $zipRel = 'module_context/dbx/modules/' . $modul . '/' . $rel;
441 if ($item->isDir()) {
442 $zip->addEmptyDir($zipRel);
443 } else {
444 $zip->addFile($full, $zipRel);
445 }
446 }
447 }
448
455 private function addReferenceToZip(\ZipArchive $zip): void {
456 $manual = dbx()->os_path(dbx()->get_base_dir() . '25_Verbindliches_Modulhandbuch.md');
457 if (is_file($manual)) {
458 $zip->addFile($manual, 'reference/25_Verbindliches_Modulhandbuch.md');
459 }
460
461 $dir = $this->moduleDir('myInvoices');
462 if (!is_dir($dir)) {
463 return;
464 }
465 $it = new \RecursiveIteratorIterator(
466 new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS),
467 \RecursiveIteratorIterator::SELF_FIRST
468 );
469 foreach ($it as $item) {
470 if (!$item->isFile()) {
471 continue;
472 }
473 $full = $item->getPathname();
474 $rel = str_replace('\\', '/', substr($full, strlen(rtrim($dir, '/\\')) + 1));
475 if (str_starts_with($rel, 'db/') || !$this->isTextFile($full) || $item->getSize() > 262144) {
476 continue;
477 }
478 $zip->addFile($full, 'reference/myInvoices/' . $rel);
479 }
480 }
481
482 private function referenceText(): string {
483 return "# Verbindlicher dbXapp-Modulstandard\n\n"
484 . "- `25_Verbindliches_Modulhandbuch.md` beschreibt Architektur und Vorgehen.\n"
485 . "- `myInvoices/` ist die ausfuehrbare Referenz fuer DD/FD, dbxDB, dbxTPL, dbxForm, dbxReport, Callbacks, Ajax, Confirm, zentrale Action-Policies und Tests.\n"
486 . "- Die Referenz ist nur lesbar. Aenderungen duerfen ausschliesslich das im Manifest genannte Zielmodul betreffen.\n"
487 . "- Bestehendes Fachverhalten und vorhandene Schnittstellen des Zielmoduls haben Vorrang vor einer blinden Kopie des Beispiels.\n";
488 }
489
490 private function startText(string $modul): string {
491 return "# START\n\n"
492 . "1. Lies `KI-AUFTRAG.md`.\n"
493 . "2. Lies `briefing.json`, `module.describe.json`, `module.snapshot.json`.\n"
494 . "3. Lies `reference/25_Verbindliches_Modulhandbuch.md` und `reference/myInvoices/` als Architekturstandard.\n"
495 . "4. Nutze `module_context/dbx/modules/$modul/` als Wahrheit fuer bestehendes Fachverhalten und Schnittstellen.\n"
496 . "5. Liefere `antwort.zip` mit `manifest.json`, `job.json`, optional `assets/` und README.\n"
497 . "6. Keine eigenen Tools, kein SQL, keine freie Dateiliste. Nur die module.* Actions aus `module.describe.json` verwenden.\n";
498 }
499
500 private function auftragText(string $modul, string $dd, string $brief): string {
501 return "# KI-Auftrag Modulprogrammierung\n\n"
502 . "Modul: `$modul`\nDD: `$dd`\n\n"
503 . "## Aufgabe\n\n" . trim($brief) . "\n\n"
504 . "## Verbindliche Regeln\n\n- " . implode("\n- ", $this->hardRules($modul)) . "\n\n"
505 . "## UI/API Regeln\n\n"
506 . "- UI-State ueber remember vars, openWin ueber data-dbx, AJAX ueber dbxAjax/json_response, Confirm ueber dbxConfirm.\n"
507 . "- Templates mit dbxTPL lesen/rendern; Antwort schreibt Templates nur via `module.template.set`.\n"
508 . "- DDs im direkt lesbaren dbxapp-Exportformat mit expliziten TABLE-, FIELDS- und INDEXES-Abschnitten schreiben; keine `\$addField`-Closure.\n"
509 . "- Nach einem Insert die neue RID in dbxForm und Action uebernehmen; erneutes Speichern muss ein Update sein.\n"
510 . "- dbxForm-Actions nie manuell mit dbx_token versehen; nur automatisch erkannte delete/save-RID-Actions werden vom System signiert.\n"
511 . "- dbxReport-Summen im {fid}_next_record-Default bilden, Footerwerte spaet per add_rep() setzen und {rpt:colspan} nutzen; keine unnoetigen Callback-Setter.\n"
512 . "- Schreibende dbxReport-Grid-Routen als `*_grid_save`, `*_grid_insert`, `*_grid_delete`, `*_grid_sort` oder `*_grid_sync` benennen; keinen eigenen Tokenmarker anfuegen.\n"
513 . "- delete/save plus rid ohne action_routes-Konfiguration ueber action_url() automatisch signieren und keine Token-Pruefung im Fachservice duplizieren.\n"
514 . "- DD->DB Sync nur via `module.dd.sync` und nur fuer `$modul|$dd`.\n\n"
515 . "## Referenz\n\n"
516 . "- `reference/25_Verbindliches_Modulhandbuch.md` ist die verbindliche Anleitung.\n"
517 . "- `reference/myInvoices/` ist das ausfuehrbare Beispiel. Nicht in das Zielmodul kopieren, sondern die passenden Muster anwenden.\n\n"
518 . "## Erlaubte job.json Actions\n\n"
519 . "- `module.backup`\n- `module.file.write`\n- `module.file.delete`\n- `module.dd.write`\n- `module.dd.sync`\n- `module.template.set`\n- `module.asset.write`\n\n"
520 . "## Antwort\n\nLiefere `antwort.zip` mit `manifest.json`, `job.json`, optional `assets/` und README. `manifest.auto_execute` bleibt true. Keine Erklaerung statt ZIP.\n";
521 }
522
523 public function handleBundleImport(): string {
524 $root = '';
525 try {
526 $form = $this->managedForm(
527 'ki-module-bundle-import',
528 'ki-module-bundle-import',
529 $this->moduleUrl('module_bundle_import')
530 );
531 if (!$form->submit()) {
532 throw new \RuntimeException('Ungueltiger oder abgelaufener Formular-Token.');
533 }
534 $payload = $this->readModuleBundlePayload();
535 $manifest = is_array($payload['manifest'] ?? null) ? $payload['manifest'] : array();
536 $job = is_array($payload['job'] ?? null) ? $payload['job'] : array();
537 $assetsDir = (string)($payload['assets_dir'] ?? '');
538 $root = (string)($payload['root'] ?? '');
539 $this->validateModulePayload($manifest, $job);
540 $result = $this->executeModuleJob($manifest, $job, $assetsDir);
541 if ($root !== '') $this->removeDir($root);
542 return $this->renderModuleBundleResult($result);
543 } catch (\Throwable $e) {
544 if ($root !== '') $this->removeDir($root);
545 dbx()->sys_msg('error', 'dbxKi', 'module_bundle_import', 'Import fehlgeschlagen', $e->getMessage());
546 return '<div class="container py-4"><div class="alert alert-danger">' . $this->esc($e->getMessage()) . '</div>'
547 . '<a class="btn btn-secondary" href="' . $this->esc($this->moduleUrl('module_bundle')) . '">Zurueck</a></div>';
548 }
549 }
550
551 private function readModuleBundlePayload(): array {
552 $file = null;
553 foreach ($_FILES as $upload) {
554 if (is_array($upload) && (int)($upload['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_OK && is_uploaded_file((string)($upload['tmp_name'] ?? ''))) {
555 $file = (string)$upload['tmp_name'];
556 break;
557 }
558 }
559 if (!$file) {
560 throw new \InvalidArgumentException('Keine Antwort-ZIP hochgeladen.');
561 }
562 if (!class_exists('\\ZipArchive')) {
563 throw new \RuntimeException('ZipArchive nicht verfuegbar.');
564 }
565 $root = rtrim(sys_get_temp_dir(), '/\\') . DIRECTORY_SEPARATOR . 'dbxki-module-' . bin2hex(random_bytes(8));
566 if (!mkdir($root, 0777, true) && !is_dir($root)) {
567 throw new \RuntimeException('Temp-Verzeichnis konnte nicht erstellt werden.');
568 }
569 $zip = new \ZipArchive();
570 if ($zip->open($file) !== true) {
571 $this->removeDir($root);
572 throw new \InvalidArgumentException('ZIP konnte nicht geoeffnet werden.');
573 }
574 for ($i = 0; $i < $zip->numFiles; $i++) {
575 $name = str_replace('\\', '/', (string)$zip->getNameIndex($i));
576 if ($name === '' || strpos($name, '../') !== false || str_starts_with($name, '/')) {
577 $zip->close();
578 $this->removeDir($root);
579 throw new \InvalidArgumentException('Ungueltiger ZIP-Pfad: ' . $name);
580 }
581 }
582 $zip->extractTo($root);
583 $zip->close();
584 $manifest = $this->readJson($root . '/manifest.json', true);
585 $job = $this->readJson($root . '/job.json', true);
586 return array(
587 'root' => $root,
588 'assets_dir' => $root . '/assets',
589 'manifest' => $manifest,
590 'job' => $job,
591 );
592 }
593
594 private function readJson(string $file, bool $required): array {
595 if (!is_file($file)) {
596 if ($required) throw new \InvalidArgumentException('Pflichtdatei fehlt: ' . basename($file));
597 return array();
598 }
599 $data = json_decode((string)file_get_contents($file), true);
600 if (!is_array($data)) {
601 throw new \InvalidArgumentException('Ungueltiges JSON: ' . basename($file));
602 }
603 return $data;
604 }
605
606 private function removeDir(string $dir): void {
607 if (!is_dir($dir)) return;
608 $it = new \RecursiveIteratorIterator(
609 new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS),
610 \RecursiveIteratorIterator::CHILD_FIRST
611 );
612 foreach ($it as $item) {
613 $item->isDir() ? @rmdir($item->getPathname()) : @unlink($item->getPathname());
614 }
615 @rmdir($dir);
616 }
617
618 private function validateModulePayload(array $manifest, array $job): void {
619 $modul = trim((string)($manifest['module'] ?? $job['module'] ?? ''));
620 if (($manifest['area'] ?? '') !== 'module' || !$this->validName($modul) || !is_dir($this->moduleDir($modul))) {
621 throw new \InvalidArgumentException('manifest.module ist ungueltig oder area ist nicht module.');
622 }
623 if (!is_array($job['steps'] ?? null) || !count($job['steps'])) {
624 throw new \InvalidArgumentException('job.json benoetigt steps[].');
625 }
626 foreach ($job['steps'] as $pos => $step) {
627 if (!is_array($step)) throw new \InvalidArgumentException('Step #' . ($pos + 1) . ' ist ungueltig.');
628 $action = trim((string)($step['action'] ?? ''));
629 if (!$this->moduleActionAllowed($action)) {
630 throw new \InvalidArgumentException('Aktion nicht erlaubt: ' . $action);
631 }
632 $stepModule = trim((string)($step['params']['module'] ?? $modul));
633 if ($stepModule !== $modul) {
634 throw new \InvalidArgumentException('Step-Modul passt nicht zum Manifest: ' . $action);
635 }
636 }
637 }
638
639 private function moduleActionAllowed(string $action): bool {
640 return in_array($action, array(
641 'module.backup',
642 'module.file.write',
643 'module.file.delete',
644 'module.dd.write',
645 'module.dd.sync',
646 'module.template.set',
647 'module.asset.write',
648 ), true);
649 }
650
651 private function executeModuleJob(array $manifest, array $job, string $assetsDir): array {
652 $modul = trim((string)($manifest['module'] ?? $job['module'] ?? ''));
653 $auto = $this->truthy($manifest['auto_execute'] ?? false);
654 if (!$auto) {
655 throw new \RuntimeException('manifest.auto_execute ist nicht true.');
656 }
657
658 $results = array();
659 $needsBackup = false;
660 foreach ($job['steps'] as $step) {
661 if (($step['action'] ?? '') !== 'module.backup') {
662 $needsBackup = true;
663 break;
664 }
665 }
666 if ($needsBackup) {
667 $results['_auto_backup'] = $this->createModuleBackup($modul);
668 }
669
670 foreach ($job['steps'] as $pos => $step) {
671 $id = trim((string)($step['id'] ?? ('step_' . ($pos + 1))));
672 $params = $this->resolveModuleParams(is_array($step['params'] ?? null) ? $step['params'] : array(), $results);
673 $params['module'] = $modul;
674 $results[$id] = $this->executeModuleStep((string)$step['action'], $params, $assetsDir);
675 }
676 return array('ok' => 1, 'module' => $modul, 'results' => $results);
677 }
678
679 private function truthy($value): bool {
680 if (is_bool($value)) return $value;
681 return in_array(strtolower(trim((string)$value)), array('1', 'true', 'yes', 'ja', 'on'), true);
682 }
683
684 private function resolveModuleParams($value, array $results) {
685 if (is_array($value)) {
686 $out = array();
687 foreach ($value as $key => $item) $out[$key] = $this->resolveModuleParams($item, $results);
688 return $out;
689 }
690 if (!is_string($value) || strpos($value, '$ref:') === false) return $value;
691 return preg_replace_callback('/\$ref:([A-Za-z0-9_.-]+)/', function($m) use ($results) {
692 $parts = explode('.', $m[1]);
693 $step = array_shift($parts);
694 $v = $results[$step] ?? null;
695 foreach ($parts as $part) {
696 $v = is_array($v) && array_key_exists($part, $v) ? $v[$part] : '';
697 }
698 return (string)$v;
699 }, $value);
700 }
701
702 private function executeModuleStep(string $action, array $params, string $assetsDir): array {
703 switch ($action) {
704 case 'module.backup':
705 return $this->createModuleBackup((string)$params['module']);
706 case 'module.file.write':
707 return $this->moduleFileWrite($params, $assetsDir);
708 case 'module.file.delete':
709 return $this->moduleFileDelete($params);
710 case 'module.dd.write':
711 return $this->moduleDdWrite($params);
712 case 'module.dd.sync':
713 return $this->moduleDdSync($params);
714 case 'module.template.set':
715 return $this->moduleTemplateSet($params);
716 case 'module.asset.write':
717 return $this->moduleAssetWrite($params, $assetsDir);
718 }
719 throw new \InvalidArgumentException('Aktion nicht implementiert: ' . $action);
720 }
721
722 private function createModuleBackup(string $modul): array {
723 $dir = dbx()->os_path(dbx()->get_file_dir() . 'temp/dbxKi/module-backups/');
724 if (!is_dir($dir) && !mkdir($dir, 0777, true) && !is_dir($dir)) {
725 throw new \RuntimeException('Backup-Verzeichnis konnte nicht erstellt werden.');
726 }
727 $file = $dir . $modul . '-' . date('Ymd-His') . '.zip';
728 $zip = new \ZipArchive();
729 if ($zip->open($file, \ZipArchive::OVERWRITE) !== true) {
730 throw new \RuntimeException('Modul-Backup konnte nicht erstellt werden.');
731 }
732 $this->addModuleToZip($zip, $modul);
733 $zip->close();
734 return array('backup' => str_replace('\\', '/', $file), 'size' => (int)@filesize($file));
735 }
736
737 private function relFromPath(string $modul, string $path): string {
738 $path = str_replace(array('\\', "\0"), array('/', ''), trim($path));
739 $prefix = 'dbx/modules/' . $modul . '/';
740 if (strpos($path, $prefix) === 0) $path = substr($path, strlen($prefix));
741 $path = ltrim($path, '/');
742 if ($path === '' || strpos($path, '../') !== false || str_starts_with($path, '/')) {
743 throw new \InvalidArgumentException('Ungueltiger Modulpfad.');
744 }
745 return $path;
746 }
747
748 private function targetFile(string $modul, string $path): string {
749 $rel = $this->relFromPath($modul, $path);
750 $file = $this->modulePath($modul, $rel);
751 if ($file === '') throw new \InvalidArgumentException('Pfad ausserhalb des Moduls.');
752 return $file;
753 }
754
755 private function bytesFromParams(array $params, string $assetsDir): string {
756 if (array_key_exists('content', $params)) return (string)$params['content'];
757 if (array_key_exists('content_base64', $params)) {
758 $bytes = base64_decode((string)$params['content_base64'], true);
759 if ($bytes === false) throw new \InvalidArgumentException('content_base64 ist ungueltig.');
760 return $bytes;
761 }
762 $assetRef = trim(str_replace('\\', '/', (string)($params['asset_ref'] ?? '')));
763 if ($assetRef !== '') {
764 if (strpos($assetRef, '../') !== false || str_starts_with($assetRef, '/')) {
765 throw new \InvalidArgumentException('asset_ref ist ungueltig.');
766 }
767 $file = rtrim($assetsDir, '/\\') . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $assetRef);
768 if (!is_file($file)) throw new \InvalidArgumentException('Asset fehlt: ' . $assetRef);
769 return (string)file_get_contents($file);
770 }
771 throw new \InvalidArgumentException('content, content_base64 oder asset_ref erforderlich.');
772 }
773
774 private function writeTargetFile(string $file, string $bytes): array {
775 $dir = dirname($file);
776 if (!is_dir($dir) && !mkdir($dir, 0777, true) && !is_dir($dir)) {
777 throw new \RuntimeException('Zielverzeichnis konnte nicht erstellt werden.');
778 }
779 if (file_put_contents($file, $bytes) === false) {
780 throw new \RuntimeException('Datei konnte nicht geschrieben werden.');
781 }
782 return array('path' => str_replace('\\', '/', $file), 'bytes' => strlen($bytes), 'sha256' => hash('sha256', $bytes));
783 }
784
785 private function moduleFileWrite(array $params, string $assetsDir): array {
786 $modul = (string)$params['module'];
787 $rel = $this->relFromPath($modul, (string)($params['path'] ?? ''));
788 if (preg_match('#^(dd/|tpl/|img/|css/|js/)#i', $rel)) {
789 throw new \InvalidArgumentException('Fuer DD/Templates/Assets die spezialisierten module.* Actions verwenden.');
790 }
791 if (preg_match('/\.(db3|sqlite|sqlite3)$/i', $rel)) {
792 throw new \InvalidArgumentException('Datenbanken werden nicht direkt geschrieben. DD->DB Sync ueber module.dd.sync verwenden.');
793 }
794 $file = $this->targetFile($modul, $rel);
795 return $this->writeTargetFile($file, $this->bytesFromParams($params, $assetsDir));
796 }
797
798 private function moduleFileDelete(array $params): array {
799 if (!$this->truthy($params['confirm'] ?? false)) {
800 throw new \InvalidArgumentException('module.file.delete benoetigt confirm=true.');
801 }
802 $modul = (string)$params['module'];
803 $file = $this->targetFile($modul, (string)($params['path'] ?? ''));
804 if (!is_file($file)) return array('deleted' => false, 'path' => str_replace('\\', '/', $file));
805 if (!unlink($file)) throw new \RuntimeException('Datei konnte nicht geloescht werden.');
806 return array('deleted' => true, 'path' => str_replace('\\', '/', $file));
807 }
808
809 private function moduleDdWrite(array $params): array {
810 $modul = (string)$params['module'];
811 $dd = trim((string)($params['dd'] ?? ''));
812 if (!$this->validName($dd)) throw new \InvalidArgumentException('Ungueltiger DD-Name.');
813 $content = (string)($params['content'] ?? '');
814 if ($content === '' || preg_match('/\b(require|include)(_once)?\b/i', $content)) {
815 throw new \InvalidArgumentException('DD muss vollstaendig sein und darf keine include/require verwenden.');
816 }
817 $file = $this->targetFile($modul, 'dd/' . $dd . '.dd.php');
818 return $this->writeTargetFile($file, $content);
819 }
820
821 private function moduleTemplateSet(array $params): array {
822 $modul = (string)$params['module'];
823 $template = trim((string)($params['template'] ?? ''));
824 $path = trim((string)($params['path'] ?? ''));
825 if ($path === '') {
826 $template = preg_replace('/[^A-Za-z0-9_.-]+/', '-', $template);
827 if ($template === '') throw new \InvalidArgumentException('template ist erforderlich.');
828 if (!preg_match('/\.htm[l]?$/i', $template)) $template .= '.htm';
829 $path = 'tpl/htm/' . $template;
830 }
831 if (strpos(str_replace('\\', '/', $path), 'tpl/') !== 0) {
832 throw new \InvalidArgumentException('module.template.set darf nur unter tpl/ schreiben.');
833 }
834 $file = $this->targetFile($modul, $path);
835 return $this->writeTargetFile($file, (string)($params['content'] ?? ''));
836 }
837
838 private function moduleAssetWrite(array $params, string $assetsDir): array {
839 $modul = (string)$params['module'];
840 $path = $this->relFromPath($modul, (string)($params['path'] ?? ''));
841 if (!preg_match('#^(tpl/(img|mod)/|img/|css/|js/)#i', $path)) {
842 throw new \InvalidArgumentException('module.asset.write darf nur tpl/img, tpl/mod, img, css oder js schreiben.');
843 }
844 return $this->writeTargetFile($this->targetFile($modul, $path), $this->bytesFromParams($params, $assetsDir));
845 }
846
847 private function moduleDdSync(array $params): array {
848 $modul = (string)$params['module'];
849 $dd = trim((string)($params['dd'] ?? ''));
850 if (!$this->validName($dd)) throw new \InvalidArgumentException('Ungueltiger DD-Name.');
851 if (!is_file($this->targetFile($modul, 'dd/' . $dd . '.dd.php'))) {
852 throw new \InvalidArgumentException('DD-Datei fehlt: ' . $modul . '|' . $dd);
853 }
854 $oDD = dbx()->get_system_obj('dbxDD');
855 if (!is_object($oDD) || !method_exists($oDD, 'sync_dd_to_db')) {
856 throw new \RuntimeException('dbxDD Sync-API nicht verfuegbar.');
857 }
858 $oDD->sync_dd_to_db($modul, $dd, 'reset');
859 $state = array();
860 for ($i = 0; $i < 80; $i++) {
861 $state = $oDD->sync_dd_to_db($modul, $dd, 'apply');
862 $status = strtolower((string)($state['status'] ?? ''));
863 if (in_array($status, array('finished', 'error', 'canceled'), true)) break;
864 if ((int)($state['percent'] ?? 0) >= 100) break;
865 }
866 return array('dd' => $modul . '|' . $dd, 'state' => $state);
867 }
868
869 private function renderModuleBundleResult(array $result): string {
870 $items = '';
871 foreach (is_array($result['results'] ?? null) ? $result['results'] : array() as $id => $row) {
872 $items .= '<li class="list-group-item"><strong>' . $this->esc($id) . '</strong><pre class="small mb-0">'
873 . $this->esc(json_encode($row, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES))
874 . '</pre></li>';
875 }
876 return '<div class="container py-4" style="max-width:1100px"><div class="alert alert-success">Modul-Bundle ausgefuehrt: '
877 . $this->esc((string)($result['module'] ?? '')) . '</div><ul class="list-group mb-3">' . $items . '</ul>'
878 . '<a class="btn btn-primary" href="' . $this->esc($this->moduleUrl('briefing_module', array('xmodul' => (string)($result['module'] ?? '')))) . '">Zur Modul-KI</a></div>';
879 }
880
881 private function modulePipelineGuide(string $modul, string $taskType = 'update'): array {
882 return array(
883 'workflow' => $this->modulePipeline($modul),
884 'rules' => $this->hardRules($modul),
885 'ui_contract' => $this->uiContract(),
886 'dbx_api_contract' => $this->dbxApiContract($modul),
887 'reference_standard' => $this->referenceStandard(),
888 'manifest' => array(
889 'bundle_version' => self::MODULE_BRIEFING_VERSION,
890 'area' => 'module',
891 'module' => $modul,
892 'recipe' => 'module.update.v1',
893 'task_type' => $taskType,
894 'auto_execute' => true,
895 ),
896 'job' => array(
897 'job_version' => self::MODULE_BRIEFING_VERSION,
898 'area' => 'module',
899 'module' => $modul,
900 'steps' => array(
901 array('id' => 'backup', 'action' => 'module.backup', 'params' => array('module' => $modul)),
902 array('id' => 'write_source', 'action' => 'module.file.write', 'params' => array('module' => $modul, 'path' => 'dbx/modules/' . $modul . '/include/Service.class.php', 'content' => '___PHP_ODER_TEXT_CONTENT___')),
903 array('id' => 'write_template', 'action' => 'module.template.set', 'params' => array('module' => $modul, 'template' => 'template-name', 'content' => '___HTML_TEMPLATE___')),
904 array('id' => 'write_dd', 'action' => 'module.dd.write', 'params' => array('module' => $modul, 'dd' => 'ddName', 'content' => '___VOLLSTAENDIGE_DD_DATEI___')),
905 array('id' => 'sync_dd', 'action' => 'module.dd.sync', 'params' => array('module' => $modul, 'dd' => 'ddName')),
906 ),
907 ),
908 );
909 }
910
911 public function handleApi(): void {
912 $raw = file_get_contents('php://input');
913 $body = array();
914 if (is_string($raw) && trim($raw) !== '') {
915 $decoded = json_decode($raw, true);
916 if (is_array($decoded)) $body = $decoded;
917 }
918 $params = is_array($body['params'] ?? null) ? $body['params'] : array();
919 $action = (string)($body['action'] ?? dbx()->get_request_var('action', 'system.describe', 'parameter+.'));
920 $modul = (string)($params['xmodul'] ?? $params['module'] ?? dbx()->get_request_var('xmodul', '', 'parameter'));
921 try {
922 switch ($action) {
923 case 'system.describe':
924 dbx()->json_response($this->moduleDescribe(''), true);
925 return;
926 case 'module.describe':
927 if (!$this->validName($modul)) throw new \InvalidArgumentException('Ungueltiges Modul');
928 dbx()->json_response($this->moduleDescribe($modul), true);
929 return;
930 case 'module.snapshot':
931 if (!$this->validName($modul)) throw new \InvalidArgumentException('Ungueltiges Modul');
932 $withContent = (int)($params['with_content'] ?? dbx()->get_request_var('with_content', 0, 'int')) === 1;
933 dbx()->json_response(array('ok' => 1, 'module' => $modul, 'files' => $this->fileTree($modul, $withContent)), true);
934 return;
935 case 'module.pipeline_guide':
936 if (!$this->validName($modul)) throw new \InvalidArgumentException('Ungueltiges Modul');
937 $taskType = (string)($params['task_type'] ?? dbx()->get_request_var('task_type', 'update', 'parameter'));
938 dbx()->json_response(array('ok' => 1, 'result' => $this->modulePipelineGuide($modul, $taskType)), true);
939 return;
940 case 'module.job.execute':
941 $manifest = is_array($params['manifest'] ?? null) ? $params['manifest'] : array();
942 $job = is_array($params['job'] ?? null) ? $params['job'] : array();
943 if (!$manifest && is_array($body['manifest'] ?? null)) $manifest = $body['manifest'];
944 if (!$job && is_array($body['job'] ?? null)) $job = $body['job'];
945 $this->validateModulePayload($manifest, $job);
946 dbx()->json_response($this->executeModuleJob($manifest, $job, ''), true);
947 return;
948 case 'module.file.read':
949 if (!$this->validName($modul)) throw new \InvalidArgumentException('Ungueltiges Modul');
950 $path = (string)($params['path'] ?? dbx()->get_request_var('path', '', '*'));
951 $prefix = 'dbx/modules/' . $modul . '/';
952 if (strpos($path, $prefix) === 0) {
953 $path = substr($path, strlen($prefix));
954 }
955 $file = $this->modulePath($modul, $path);
956 if ($file === '' || !is_file($file) || !$this->isTextFile($file)) {
957 throw new \InvalidArgumentException('Datei nicht lesbar');
958 }
959 dbx()->json_response(array('ok' => 1, 'module' => $modul, 'path' => 'dbx/modules/' . $modul . '/' . str_replace('\\', '/', $path), 'content' => (string)file_get_contents($file)), true);
960 return;
961 }
962 throw new \InvalidArgumentException('Unbekannte action');
963 } catch (\Throwable $e) {
964 dbx()->json_response(array('ok' => 0, 'error' => $e->getMessage()), true);
965 }
966 }
967}
update($dd, $field_values, $where, $verify_access=1, $verify_fields=1, $verify_values=1, $trace=1)
Führt ein UPDATE-Statement auf einer Tabelle aus.
dbx()
Liefert die zentrale dbXapp-API als Singleton.
Definition dbxApi.php:2805
get_base_dir(int $cutData=0)
Liefert das Basisverzeichnis der Installation.
Definition dbxApi.php:1507
DBX schema administration.