dbxapp 4.1.3
CMS, Shop, Workflows und modulare Geschäftsanwendungen
Loading...
Searching...
No Matches
dbxWizard.class.php
Go to the documentation of this file.
1<?php
2namespace dbx\dbxAdmin;
3
4class dbxWizard {
5
6 private $texts;
7
8 private function texts() {
9 if ($this->texts) {
10 return $this->texts;
11 }
12 $texts = new \dbxForm();
13 $texts->set_form_help_enabled(false);
14 $texts->_fd = 'dbxAdmin|module-wizard';
15 $texts->load_fd_messages();
16 $this->texts = $texts;
17 return $this->texts;
18 }
19
20 private function text($key, $default = '') {
21 return $this->texts()->get_fd_message((string)$key, (string)$default);
22 }
23
24 private function esc($value) {
25 return htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8');
26 }
27
28 private function modules_root() {
29 return dbx()->os_path(dbx()->get_base_dir() . 'dbx/modules/');
30 }
31
32 private function backup_root() {
33 $dir = dbx()->os_path(dbx()->get_file_dir() . 'module-backup/');
34 if (!is_dir($dir)) {
35 @mkdir($dir, 0777, true);
36 }
37 return $dir;
38 }
39
40 private function valid_name($name) {
41 return is_string($name) && preg_match('/^[A-Za-z][A-Za-z0-9_]{1,63}$/', $name);
42 }
43
44 private function valid_file_name($name) {
45 return is_string($name) && preg_match('/^[A-Za-z0-9_.-]+$/', $name) && strpos($name, '..') === false;
46 }
47
48 private function module_dir($modul) {
49 return dbx()->os_path($this->modules_root() . $modul . DIRECTORY_SEPARATOR);
50 }
51
52 private function module_path($modul, $rel) {
53 $rel = str_replace(array('\\', "\0"), array('/', ''), (string)$rel);
54 $rel = ltrim($rel, '/');
55 if ($rel === '' || strpos($rel, '../') !== false || strpos($rel, '..\\') !== false) {
56 return '';
57 }
58 $base = $this->module_dir($modul);
59 $path = dbx()->os_path($base . $rel);
60 $baseNorm = str_replace('\\', '/', rtrim($base, '/\\') . '/');
61 $pathNorm = str_replace('\\', '/', $path);
62 if (strpos($pathNorm, $baseNorm) !== 0) {
63 return '';
64 }
65 return $path;
66 }
67
68 private function module_options($withNew = false) {
69 $options = $withNew ? array('' => $this->text('option_choose')) : array();
70 $root = $this->modules_root();
71 foreach (glob($root . '*', GLOB_ONLYDIR) ?: array() as $dir) {
72 $name = basename($dir);
73 if ($this->valid_name($name)) {
74 $options[$name] = $name;
75 }
76 }
77 ksort($options);
78 return $options;
79 }
80
81 private function dd_options($withEmpty = true) {
82 $options = $withEmpty ? array('' => $this->text('option_choose_manual')) : array();
83 foreach (glob($this->modules_root() . '*', GLOB_ONLYDIR) ?: array() as $dir) {
84 $modul = basename($dir);
85 if (!$this->valid_name($modul)) {
86 continue;
87 }
88 foreach (glob(dbx()->os_path($dir . '/dd/') . '*.dd.php') ?: array() as $file) {
89 $dd = basename($file, '.dd.php');
90 if ($this->valid_name($dd)) {
91 $options[$modul . '|' . $dd] = $modul . ' | ' . $dd;
92 }
93 }
94 }
95 ksort($options);
96 return $options;
97 }
98
99 private function options($items) {
100 return is_array($items) ? $items : array();
101 }
102
103 private function collect_input($form = null) {
104 $get = function($name, $default = '', $rules = 'parameter') use ($form) {
105 if ($form && array_key_exists($name, $form->_post)) {
106 return $form->_post[$name];
107 }
108 return dbx()->get_modul_var($name, $default, $rules);
109 };
110
111 $target = (string)$get('target_mode', 'new');
112 $existingModul = trim((string)$get('existing_modul', ''));
113 $modul = trim((string)$get('xmodul', ''));
114 if ($target === 'existing' && $existingModul !== '') {
115 $modul = $existingModul;
116 }
117 if ($modul === '') {
118 $modul = trim((string)$get('modul', ''));
119 }
120
121 $ddMode = (string)$get('dd_mode', 'new');
122 $ddRef = trim((string)$get('dd_ref', ''));
123 $title = trim((string)$get('title', $modul));
124 $dd = trim((string)$get('dd_name', $modul ? $modul . 'Data' : ''));
125 if ($ddRef !== '' && strpos($ddRef, '|') !== false) {
126 list($refModul, $refDd) = explode('|', $ddRef, 2);
127 if ($target === 'existing' && $modul === '' && $this->valid_name($refModul)) {
128 $modul = $refModul;
129 }
130 if ($this->valid_name($refDd)) {
131 $dd = $refDd;
132 }
133 }
134 if ($dd === '' && $ddMode !== 'none' && $modul !== '') {
135 $dd = $modul . 'Data';
136 }
137 $table = trim((string)$get('table_name', $dd));
138 if ($table === '' && $ddMode !== 'none') {
139 $table = $dd;
140 }
141 $dbFile = trim((string)$get('db_file', $modul ? $modul . '.db3' : 'modul.db3'));
142 if ($modul !== '' && ($dbFile === '' || $dbFile === 'modul.db3')) {
143 $dbFile = $modul . '.db3';
144 }
145 $defaultRun1 = trim((string)$get('default_run1', 'run'));
146 if ($defaultRun1 === '') {
147 $defaultRun1 = 'run';
148 }
149
150 return array(
151 'target_mode' => in_array($target, array('new', 'existing'), true) ? $target : 'new',
152 'existing_modul' => $existingModul,
153 'dd_ref' => $ddRef,
154 'xmodul' => $modul,
155 'title' => $title !== '' ? $title : $modul,
156 'default_run1' => $defaultRun1,
157 'default_run2' => trim((string)$get('default_run2', '')),
158 'module_template' => (string)$get('module_template', 'form_report'),
159 'dd_mode' => $ddMode,
160 'dd_name' => $dd,
161 'table_name' => $table,
162 'db_file' => $dbFile,
163 'field_preset' => (string)$get('field_preset', 'basic'),
164 'create_include' => (int)$get('create_include', 1, 'int'),
165 'create_form' => (int)$get('create_form', 1, 'int'),
166 'create_report' => (int)$get('create_report', 1, 'int'),
167 'create_templates' => (int)$get('create_templates', 1, 'int'),
168 'overwrite' => (int)$get('overwrite', 0, 'int'),
169 'backup' => (int)$get('backup', 1, 'int'),
170 'sync_mode' => (string)$get('sync_mode', 'link'),
171 'ki_package' => (int)$get('ki_package', 1, 'int'),
172 );
173 }
174
175 private function validate_input(array $in, &$errors) {
176 $errors = array();
177 if (!$this->valid_name($in['xmodul'])) {
178 $errors[] = $this->text('validation_module_name');
179 }
180 if (!in_array($in['module_template'], array('blank', 'form', 'report', 'form_report', 'api'), true)) {
181 $errors[] = $this->text('validation_template');
182 }
183 if (!in_array($in['dd_mode'], array('none', 'new', 'existing'), true)) {
184 $errors[] = $this->text('validation_dd_mode');
185 }
186 if (((int)$in['create_form'] === 1 || (int)$in['create_report'] === 1) && $in['dd_mode'] === 'none') {
187 $errors[] = $this->text('validation_dd_required');
188 }
189 if (($in['module_template'] === 'api' || (int)$in['create_form'] === 1 || (int)$in['create_report'] === 1) && (int)$in['create_include'] !== 1) {
190 $errors[] = $this->text('validation_service_required');
191 }
192 if (((int)$in['create_form'] === 1 || (int)$in['create_report'] === 1) && (int)$in['create_templates'] !== 1) {
193 $errors[] = $this->text('validation_templates_required');
194 }
195 if ($in['dd_mode'] !== 'none' && !$this->valid_name($in['dd_name'])) {
196 $errors[] = $this->text('validation_dd_name');
197 }
198 if ($in['dd_mode'] !== 'none' && !$this->valid_name($in['table_name'])) {
199 $errors[] = $this->text('validation_table_name');
200 }
201 if ($in['dd_mode'] !== 'none' && (!$this->valid_file_name($in['db_file']) || !preg_match('/\.db3$/i', $in['db_file']))) {
202 $errors[] = $this->text('validation_db_file');
203 }
204 if ($in['dd_mode'] === 'existing') {
205 if ($in['dd_ref'] !== '' && strpos($in['dd_ref'], '|') !== false) {
206 list($refModul, $refDd) = explode('|', $in['dd_ref'], 2);
207 if ($refModul !== $in['xmodul'] || $refDd !== $in['dd_name']) {
208 $errors[] = $this->text('validation_dd_module');
209 }
210 }
211 $ddFile = $this->module_path($in['xmodul'], 'dd/' . $in['dd_name'] . '.dd.php');
212 if ($ddFile === '' || !is_file($ddFile)) {
213 $errors[] = $this->text('validation_dd_missing');
214 }
215 }
216
217 $dir = $this->module_dir($in['xmodul']);
218 if ($in['target_mode'] === 'new' && is_dir($dir)) {
219 $errors[] = $this->text('validation_module_exists');
220 }
221 if ($in['target_mode'] === 'existing' && !is_dir($dir)) {
222 $errors[] = $this->text('validation_module_missing');
223 }
224
225 return !count($errors);
226 }
227
228 private function ensure_dirs($modul, array &$log) {
229 $ok = true;
230 $root = $this->module_dir($modul);
231 if (!is_dir($root) && !@mkdir($root, 0777, true) && !is_dir($root)) {
232 $log[] = array('type' => 'error', 'text' => 'Modulverzeichnis konnte nicht erstellt werden.');
233 return false;
234 }
235 foreach (array('cfg', 'dd', 'db', 'fd', 'include', 'tpl', 'tpl/htm', 'tpl/mod', 'tpl/css', 'tpl/js', 'tpl/img') as $dir) {
236 $path = $this->module_path($modul, $dir);
237 if ($path === '' || (!is_dir($path) && !@mkdir($path, 0777, true) && !is_dir($path))) {
238 $log[] = array('type' => 'error', 'text' => 'Verzeichnis konnte nicht erstellt werden: ' . $dir);
239 $ok = false;
240 }
241 }
242 return $ok;
243 }
244
245 private function write_module_file($modul, $rel, $content, $overwrite, array &$log) {
246 $path = $this->module_path($modul, $rel);
247 if ($path === '') {
248 $log[] = array('type' => 'error', 'text' => 'Pfad blockiert: ' . $rel);
249 return false;
250 }
251 $dir = dirname($path);
252 if (!is_dir($dir)) {
253 @mkdir($dir, 0777, true);
254 }
255 if (is_file($path) && !$overwrite) {
256 $log[] = array('type' => 'skip', 'text' => $rel . ' existiert bereits.');
257 return true;
258 }
259 $ok = file_put_contents($path, $content) !== false;
260 $log[] = array('type' => $ok ? 'ok' : 'error', 'text' => ($ok ? 'geschrieben: ' : 'Fehler: ') . $rel);
261 return $ok;
262 }
263
264 private function backup_module($modul) {
265 if (!class_exists('\\ZipArchive')) {
266 return '';
267 }
268 $dir = $this->module_dir($modul);
269 if (!is_dir($dir)) {
270 return '';
271 }
272 $file = date('Ymd-His') . '__' . $modul . '.zip';
273 $path = $this->backup_root() . $file;
274 $zip = new \ZipArchive();
275 if ($zip->open($path, \ZipArchive::OVERWRITE) !== true) {
276 return '';
277 }
278 $it = new \RecursiveIteratorIterator(
279 new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS),
280 \RecursiveIteratorIterator::SELF_FIRST
281 );
282 foreach ($it as $item) {
283 $full = $item->getPathname();
284 $rel = str_replace('\\', '/', substr($full, strlen(rtrim($dir, '/\\')) + 1));
285 if ($item->isDir()) {
286 $zip->addEmptyDir($rel);
287 } else {
288 $zip->addFile($full, $rel);
289 }
290 }
291 $zip->close();
292 return $file;
293 }
294
295 private function restore_module_backup() {
296 $modul = dbx()->get_modul_var('xmodul', '', 'parameter');
297 $file = basename((string)dbx()->get_modul_var('file', '', 'parameter+.-_'));
298 $confirm = (int)dbx()->get_modul_var('confirm', 0, 'int');
299 if (!$this->valid_name($modul) || !$this->valid_file_name($file)) {
300 return '<div class="alert alert-danger">Restore nicht moeglich: ungueltige Parameter.</div>';
301 }
302 $zipPath = $this->backup_root() . $file;
303 if (!is_file($zipPath)) {
304 return '<div class="alert alert-warning">Backup-ZIP nicht gefunden.</div>';
305 }
306 if (!$confirm) {
307 $url = '?dbx_modul=dbxAdmin&dbx_run1=modules&dbx_run2=modul_new&dbx_run3=restore_backup&xmodul=' . rawurlencode($modul) . '&file=' . rawurlencode($file) . '&confirm=1';
308 return '<div class="p-3"><h3>Modul wiederherstellen</h3><p>Restore ersetzt den Inhalt von <code>dbx/modules/' . $this->esc($modul) . '</code> durch das Backup <code>' . $this->esc($file) . '</code>.</p><a class="btn btn-danger" href="' . $this->esc($url) . '">Restore starten</a></div>';
309 }
310
311 $currentBackup = $this->backup_module($modul);
312 $dir = $this->module_dir($modul);
313 if (!is_dir($dir)) {
314 @mkdir($dir, 0777, true);
315 }
316 $this->empty_dir($dir);
317 $zip = new \ZipArchive();
318 if ($zip->open($zipPath) !== true) {
319 return '<div class="alert alert-danger">Backup konnte nicht geoeffnet werden.</div>';
320 }
321 for ($i = 0; $i < $zip->numFiles; $i++) {
322 $name = str_replace('\\', '/', (string)$zip->getNameIndex($i));
323 if ($name === '' || strpos($name, '../') !== false || strpos($name, '..\\') !== false || $name[0] === '/') {
324 $zip->close();
325 return '<div class="alert alert-danger">Backup enthaelt ungueltige Pfade.</div>';
326 }
327 }
328 $zip->extractTo($dir);
329 $zip->close();
330 return '<div class="alert alert-success">Modul wiederhergestellt. Sicherheitsbackup vorher: ' . $this->esc($currentBackup) . '</div>';
331 }
332
333 private function empty_dir($dir) {
334 if (!is_dir($dir)) return;
335 $it = new \RecursiveIteratorIterator(
336 new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS),
337 \RecursiveIteratorIterator::CHILD_FIRST
338 );
339 foreach ($it as $item) {
340 if ($item->isDir()) {
341 @rmdir($item->getPathname());
342 } else {
343 @unlink($item->getPathname());
344 }
345 }
346 }
347
348 private function field_defs($preset) {
349 $fields = array(
350 array('id', 'int', '11', '', 'ID', 'int', 'hidden', 'PRI', ''),
351 array('create_date', 'datetime', '-1', '', 'Erstellt', 'datetime', 'hidden', '', ''),
352 array('create_uid', 'int', '11', '0', 'Erstellt von', 'int', 'hidden', 'MUL', ''),
353 array('update_date', 'datetime', '-1', '', 'Aktualisiert', 'datetime', 'hidden', '', ''),
354 array('update_uid', 'int', '11', '0', 'Aktualisiert von', 'int', 'hidden', 'MUL', ''),
355 array('owner', 'int', '11', '0', 'Owner', 'int', 'hidden', 'MUL', ''),
356 array('trash', 'int', '1', '0', 'Trash', 'int', 'hidden', 'MUL', ''),
357 array('activ', 'int', '1', '1', 'Aktiv', 'int', 'checkbox-label', 'MUL', ''),
358 array('sorter', 'varchar', '16', '', 'Sortierung', 'varchar', 'text-label', '', ''),
359 array('title', 'varchar', '254', '', 'Titel', 'varchar|min=1|max=254', 'text-label', 'MUL', ''),
360 array('description', 'text', '-1', '', 'Beschreibung', 'text', 'textarea-label', '', ''),
361 );
362 if (in_array($preset, array('content', 'workflow'), true)) {
363 $fields[] = array('content', 'text', '-1', '', 'Inhalt', 'text', 'textarea-label', '', '');
364 }
365 if (in_array($preset, array('status', 'workflow'), true)) {
366 $fields[] = array('status', 'varchar', '32', 'open', 'Status', 'parameter|max=32', 'select-single-label', 'MUL', array('open' => 'Offen', 'review' => 'Pruefung', 'done' => 'Erledigt', 'archived' => 'Archiv'));
367 }
368 return $fields;
369 }
370
371 private function dd_field_php(array $f) {
372 list($name, $type, $length, $default, $label, $rules, $tpl, $index) = array_pad($f, 8, '');
373 $options = $f[8] ?? '';
374 return "\$field['name']=" . var_export($name, true) . ";\n"
375 . "\$field['type']=" . var_export($type, true) . ";\n"
376 . "\$field['index']=" . var_export($index, true) . ";\n"
377 . "\$field['length']=" . var_export($length, true) . ";\n"
378 . "\$field['default']=" . var_export($default, true) . ";\n"
379 . "\$field['label']=" . var_export($label, true) . ";\n"
380 . "\$field['rules']=" . var_export($rules, true) . ";\n"
381 . "\$field['tooltip']='';\n\$field['errormsg']='';\n\$field['placeholder']='';\n\$field['convert']='';\n\$field['protect']='0';\n\$field['group']='';\n\$field['mask']='';\n\$field['data']='';\n"
382 . "\$field['options']=" . var_export($options, true) . ";\n"
383 . "\$field['tpl']=" . var_export($tpl, true) . ";\n"
384 . "\$field['js']='';\n\$field['prompt']='';\n\$fields[]=\$field;\n\n";
385 }
386
387 private function generate_dd(array $in) {
388 $out = "<?php\n\n";
389 $out .= "/* =========================================================\n TABLE\n ========================================================= */\n";
390 $out .= "\$table['server']=" . var_export($in['xmodul'] . '|' . $in['db_file'], true) . ";\n";
391 $out .= "\$table['table']=" . var_export($in['table_name'], true) . ";\n";
392 $out .= "\$table['datadic']=" . var_export($in['dd_name'], true) . ";\n";
393 $out .= "\$table['primary']='id';\n\$table['language']='0';\n\$table['version']='1.0';\n\$table['autosync']='0';\n\$table['cache']='0';\n\$table['trash']='1';\n\$table['trace']='0';\n\$table['update_sql']='';\n\$table['default_sort']='title ASC';\n\$table['form-dd-table']='';\n\$table['read']='admin';\n\$table['create']='admin';\n\$table['update']='admin';\n\$table['delete']='admin';\n\$table['read_owner']='owner,admin';\n\$table['create_owner']='owner,admin';\n\$table['update_owner']='owner,admin';\n\$table['delete_owner']='owner,admin';\n\n";
394 $out .= "/* =========================================================\n FIELDS\n ========================================================= */\n";
395 foreach ($this->field_defs($in['field_preset']) as $field) {
396 $out .= $this->dd_field_php($field);
397 }
398 $out .= "/* =========================================================\n INDEXES\n ========================================================= */\n";
399 $indexes = array(
400 array('idx_' . $in['table_name'] . '_title', 'title'),
401 array('idx_' . $in['table_name'] . '_activ', 'activ'),
402 );
403 if (in_array($in['field_preset'], array('status', 'workflow'), true)) {
404 $indexes[] = array('idx_' . $in['table_name'] . '_status', 'status');
405 }
406 foreach ($indexes as $idx) {
407 $out .= "\$index['name']=" . var_export($idx[0], true) . ";\n\$index['type']='INDEX';\n\$index['fields']=" . var_export($idx[1], true) . ";\n\$index['unique']='0';\n\$index['comment']='module wizard';\n\$indexes[]=\$index;\n\n";
408 }
409 return $out;
410 }
411
412 private function generate_form_fd(array $in) {
413 $out = "<?php\n"
414 . "\$messages = array();\n"
415 . "\$messages['save_success'] = 'Daten wurden gespeichert';\n"
416 . "\$messages['save_succeass'] = \$messages['save_success'];\n"
417 . "\$messages['save_error'] = 'Daten konnten nicht gespeichert werden';\n\n";
418 foreach ($this->field_defs($in['field_preset']) as $f) {
419 if (in_array($f[0], array('id', 'create_date', 'create_uid', 'update_date', 'update_uid', 'owner', 'trash'), true)) {
420 continue;
421 }
422 $out .= $this->dd_field_php($f);
423 }
424 return $out;
425 }
426
427 private function generate_report_fd() {
428 return <<<'PHP'
429<?php
430$messages = array();
431$messages['save_success'] = 'Daten wurden gespeichert';
432$messages['save_succeass'] = $messages['save_success'];
433$messages['save_error'] = 'Daten konnten nicht gespeichert werden';
434
435$field['name']='dbx_rrows';
436$field['type']='int';
437$field['tpl']='select-single-label';
438$field['default']='20';
439$field['label']='Anz.Seite';
440$field['rules']='int';
441$field['options']='10=10&20=20&50=50&100=100&0=alle';
443
444$field['name']='dbx_rsort';
445$field['type']='varchar';
446$field['tpl']='select-single-label';
447$field['default']='title';
448$field['label']='Sortierung';
449$field['rules']='sqlsearch';
450$field['options']='id=id&title=Titel&update_date=Update&sorter=Sortierung&activ=Aktiv';
452
453$field['name']='dbx_rdesc';
454$field['type']='varchar';
455$field['tpl']='select-single-label';
456$field['default']='ASC';
457$field['label']='Auf/Ab';
458$field['rules']='parameter';
459$field['options']='ASC=Aufsteigend&DESC=Absteigend';
461
462$field['name']='dbx_rwhere';
463$field['type']='varchar';
464$field['tpl']='dbx|search';
465$field['default']='';
466$field['label']='Suchen';
467$field['rules']='parameter';
468$field['options']='';
470
471$field['name']='dbx_rselect';
472$field['type']='int';
473$field['tpl']='select-single-label';
474$field['default']='0';
475$field['label']='Auswahl';
476$field['rules']='int';
477$field['options']='0=Alle&1=Ausgewaehlte';
479PHP;
480 }
481
482 private function generate_form_template() {
483 return <<<'HTML'
484<div id="dbxForm_{i}" class="dbx-panel dbxForm_wrapper">
485 <form action="{action}" method="post" id="dbx_form_{i}" class="dbxAjax" data-target="dbxForm_{i}">
486 [tpl=dbx|module-bar]
487 <div class="dbx-panel-body">
488 <div class="mb-3">{obj:form_msg}</div>
489 <div class="row g-3">[dbx:form]</div>
490 </div>
491 [dbx:js]
492 </form>
493</div>
494HTML;
495 }
496
497 private function generate_report_template() {
498 return <<<'HTML'
499[tpl=dbx|report-shell-head]
500 [dbx:pagination]
501 <div class="table-responsive">
502 <table class="table table-striped table-bordered table-light table-hover align-middle">
503 <thead>
504 <tr class="{tr-class}">[rpt:row]</tr>
505 </thead>
506 <tbody>
507 <hr class="dbx_split">
508 <tr class="{tr-class}">[rpt:row]</tr>
509 <hr class="dbx_split">
510 </tbody>
511 </table>
512 </div>
513[tpl=dbx|report-shell-foot]
514HTML;
515 }
516
517 private function generate_main_class(array $in) {
518 $modul = $in['xmodul'];
519 $service = $modul . 'Service';
520 $default = $in['default_run1'] ?: 'run';
521 $startContent = $this->esc('Modul ' . $modul . ' ist bereit.');
522 $startMethod = " private function start() {\n \$tpl = dbx()->get_system_obj('dbxTPL');\n return \$tpl->get_tpl('$modul|start', array('content' => " . var_export($startContent, true) . "));\n }\n";
523 if ((int)$in['create_templates'] !== 1) {
524 $startMethod = " private function start() {\n return '<div class=\"p-3\"><h2>" . $this->esc($in['title']) . "</h2><p>" . $startContent . "</p></div>';\n }\n";
525 }
526 $cases = '';
527 $cases .= " case " . var_export($default, true) . ":\n \$content = \$this->start();\n break;\n\n";
528 if ($in['create_form']) {
529 $cases .= " case 'form':\n \$content = \$this->service()->form();\n break;\n\n";
530 }
531 if ($in['create_report']) {
532 $cases .= " case 'report':\n \$content = \$this->service()->report();\n break;\n\n";
533 $cases .= " case 'detail':\n \$content = \$this->service()->detail();\n break;\n\n";
534 }
535 if ($in['module_template'] === 'api' || $in['create_include']) {
536 $cases .= " case 'api':\n \$this->service()->api();\n break;\n\n";
537 }
538 return "<?php\nnamespace dbx\\$modul;\n\nclass $modul {\n\n private function service() {\n return dbx()->get_include_obj('$service', '$modul');\n }\n\n$startMethod\n public function run() {\n \$run = dbx()->get_modul_var('dbx_run1', " . var_export($default, true) . ", 'parameter');\n \$content = '';\n switch (\$run) {\n$cases default:\n \$content = '<div class=\"alert alert-warning\">Unbekannter Aufruf: ' . htmlspecialchars((string)\$run, ENT_QUOTES, 'UTF-8') . '</div>';\n }\n return \$content;\n }\n}\n";
539 }
540
541 private function generate_service_class(array $in) {
542 $modul = $in['xmodul'];
543 $class = $modul . 'Service';
544 $ddRef = $in['dd_mode'] !== 'none' ? $modul . '|' . $in['dd_name'] : '';
545 $formFd = $modul . '|' . $in['dd_name'] . '-form';
546 $reportFd = $modul . '|rpt-' . $in['dd_name'] . '-selection';
547 $fields = array();
548 foreach ($this->field_defs($in['field_preset']) as $f) {
549 if (in_array($f[0], array('id', 'title', 'description', 'content', 'status', 'activ', 'update_date'), true)) {
550 $fields[$f[0]] = $f[4];
551 }
552 }
553 $fieldsExport = var_export($fields, true);
554 $template = <<<'PHP'
555<?php
556namespace dbx\__MODUL__;
557
569class __CLASS__ {
570
571 private $dd = __DD_REF__;
572 private $formFd = __FORM_FD__;
573 private $reportFd = __REPORT_FD__;
574
575 private function db() {
576 return dbx()->get_system_obj('dbxDB');
577 }
578
579 private function tpl() {
580 return dbx()->get_system_obj('dbxTPL');
581 }
582
583 private function base_url($run1 = 'report', array $params = array()) {
584 $url = '?dbx_modul=__MODUL__&dbx_run1=' . rawurlencode($run1);
585 foreach ($params as $key => $value) {
586 $url .= '&' . rawurlencode((string)$key) . '=' . rawurlencode((string)$value);
587 }
588 return $url;
589 }
590
601 public function form($rid = null) {
602 if ($this->dd === '') return '<div class="alert alert-info">Kein DD konfiguriert.</div>';
603
604 $rid = $rid === null ? (int)dbx()->get_modul_var('rid', 0, 'int') : (int)$rid;
605 $do = dbx()->get_modul_var('dbx_do', '', 'parameter');
606
607 if ($do === 'delete' && $rid > 0) {
608 return $this->delete_record($rid);
609 }
610
611 $data = $rid > 0 ? $this->db()->select1($this->dd, $rid) : array('activ' => 1);
612 if (!is_array($data)) $data = array('activ' => 1);
613
614 $form = dbx()->get_system_obj('dbxForm');
615 $form->init('__MODUL__-form');
616 $form->set_form_callback_owner($this);
617 $form->set_init_callback('form_init_callback');
618 $form->set_submit_callback('form_submit_callback');
619 $form->set_run_callback('form_run_callback');
620
621 $form->_dd = $this->dd;
622 $form->_fd = $this->formFd;
623 $form->_data = $data;
624 $form->_rid = $rid;
625 $form->_action = $this->base_url('form', $rid > 0 ? array('rid' => $rid) : array());
626 $form->add_rep('bar_title', $rid > 0 ? 'Datensatz bearbeiten' : 'Neuer Datensatz');
627 $form->add_rep('bar_subtitle', $this->dd);
628 $form->add_obj('bar_actions', 'obj-value', $this->form_action_buttons($rid));
629 $form->_msg_info = 'Felder ausfuellen und mit Speichern uebernehmen.';
630
631 $this->configure_form_fields($form);
632
633 if ($form->submit() && !$form->errors()) {
634 $ok = $form->save_post($this->dd, $rid > 0 ? $rid : 'new', $this->form_save_defaults($rid));
635 if ($ok) {
636 $form->_msg_success = 'Datensatz gespeichert.';
637 } else {
638 $form->_msg_error = 'Datensatz konnte nicht gespeichert werden.';
639 }
640 }
641
642 return $form->run();
643 }
644
645 private function form_action_buttons($rid) {
646 $html = '<button class="btn btn-primary btn-sm" type="submit"><i class="bi bi-save"></i> Speichern</button>';
647 if ($rid > 0) {
648 $delete = $this->base_url('form', array('rid' => $rid, 'dbx_do' => 'delete'));
649 $html .= ' <a class="btn btn-outline-danger btn-sm dbxConfirm" href="' . htmlspecialchars($delete, ENT_QUOTES, 'UTF-8') . '" data-confirm-title="Datensatz loeschen" data-confirm="Diesen Datensatz wirklich loeschen?" data-confirm-buttons="yesno"><i class="bi bi-trash"></i> Loeschen</a>';
650 }
651 $html .= ' <a class="btn btn-outline-secondary btn-sm" href="' . htmlspecialchars($this->base_url('report'), ENT_QUOTES, 'UTF-8') . '"><i class="bi bi-table"></i> Report</a>';
652 return $html;
653 }
654
655 private function configure_form_fields($form) {
656 $form->add_flds();
657
658 /*
659 * Gängige Feldtemplates fuer FD/DD:
660 * - text-label: einzeiliger Text
661 * - textarea-label: mehrzeiliger Text / HTML je nach Regeln
662 * - checkbox-label: 0/1
663 * - select-single-label: einfache Auswahl mit options array(...)
664 * - select-multible-label / multiselect2: Mehrfachauswahl, name[]
665 * - date-label, integer-label, password-label, hidden
666 *
667 * Direktes Feld-Beispiel ohne FD:
668 * $form->add_fld('status', 'select-single-label', label: 'Status', rules: 'parameter', options: array('open' => 'Offen', 'done' => 'Erledigt'));
669 */
670 }
671
672 private function form_save_defaults($rid) {
673 $uid = (int)dbx()->user();
674 $now = date('Y-m-d H:i:s');
675 $defaults = array('update_date' => $now, 'update_uid' => $uid);
676 if ((int)$rid <= 0) {
677 $defaults['create_date'] = $now;
678 $defaults['create_uid'] = $uid;
679 $defaults['owner'] = $uid;
680 $defaults['trash'] = 0;
681 }
682 return $defaults;
683 }
684
685 public function delete_record($rid) {
686 $rid = (int)$rid;
687 if ($rid <= 0) return '<div class="alert alert-warning">Kein Datensatz gewaehlt.</div>';
688 $ok = $this->db()->delete($this->dd, $rid);
689 if ($ok) {
690 return '<div class="alert alert-success">Datensatz geloescht.</div>' . $this->report();
691 }
692 return '<div class="alert alert-danger">Datensatz konnte nicht geloescht werden.</div>' . $this->report();
693 }
694
695 public function detail($rid = null) {
696 $rid = $rid === null ? (int)dbx()->get_modul_var('rid', 0, 'int') : (int)$rid;
697 if ($rid <= 0) return '<div class="alert alert-warning">Kein Datensatz gewaehlt.</div>';
698 $record = $this->db()->select1($this->dd, $rid);
699 if (!is_array($record)) return '<div class="alert alert-warning">Datensatz nicht gefunden.</div>';
700
701 $rows = '';
702 foreach ($record as $key => $value) {
703 $rows .= '<tr><th>' . htmlspecialchars((string)$key, ENT_QUOTES, 'UTF-8') . '</th><td>' . htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8') . '</td></tr>';
704 }
705 return '<div class="p-3"><h3>Detail #' . $rid . '</h3><table class="table table-sm table-bordered">' . $rows . '</table><p><a class="btn btn-primary btn-sm" href="' . htmlspecialchars($this->base_url('form', array('rid' => $rid)), ENT_QUOTES, 'UTF-8') . '">Bearbeiten</a> <a class="btn btn-outline-secondary btn-sm" href="' . htmlspecialchars($this->base_url('report'), ENT_QUOTES, 'UTF-8') . '">Zurueck</a></p></div>';
706 }
707
717 public function report() {
718 if ($this->dd === '') return '<div class="alert alert-info">Kein DD konfiguriert.</div>';
719
720 $report = dbx()->get_system_obj('dbxReport');
721 $report->init('__MODUL__-report');
722 $report->set_callback_owner($this);
723 $report->set_report_header_callback('report_header_callback');
724 $report->set_page_header_callback('report_page_header_callback');
725 $report->set_header_callback('report_table_header_callback');
726 $report->set_body_callback('report_body_callback');
727 $report->set_footer_callback('report_table_footer_callback');
728 $report->set_page_footer_callback('report_page_footer_callback');
729 $report->set_report_footer_callback('report_footer_callback');
730 $report->set_next_record_callback('report_next_record_callback');
731 $report->set_callback('row_action_data', 'report_row_action_data_callback');
732
733 $report->_dd = $this->dd;
734 $report->_action = $this->base_url('report');
735 $report->_pages = true;
736 $report->_mode = 'table';
737 $report->_but_pagination = 9;
738 $report->_multi_page_select = 1;
739 $report->_create_sel_flds = true;
740 $report->_create_row_select = true;
741 $report->_create_row_edit = true;
742 $report->_create_row_show = true;
743 $report->_create_row_delete = true;
744 $report->_msg_confirm_delete = 'Diesen Datensatz wirklich loeschen?';
745 $report->add_rep('bar_title', 'Datensaetze');
746 $report->add_rep('bar_subtitle', $this->dd);
747 $report->add_rep('bar_icon', 'bi-table');
748 $report->add_rep('bar_class', 'dbx-module-bar');
749 $report->add_rep('bar_title_class', 'dbx-module-bar-titleblock');
750 $report->add_rep('bar_actions_class', 'dbx-module-bar-actions');
751 $report->add_rep('bar_title_pre', '');
752 $report->add_rep('bar_title_heading_attrs', '');
753 $report->add_rep('bar_middle', '');
754 $report->add_rep('bar_extra', '');
755 $report->add_rep('bar_actions', '<a class="btn btn-primary btn-sm" href="' . htmlspecialchars($this->base_url('form'), ENT_QUOTES, 'UTF-8') . '"><i class="bi bi-plus-lg"></i> Neu</a>');
756
757 $report->add_action('rows_select', 'action_button_select', '&dbx_do=rows_select');
758 $report->add_action('rows_deselect', 'action_button_deselect', '&dbx_do=clear_selects');
759 $report->add_action('rows_delete', 'action_button_delete', '&dbx_do=multi_delete');
760 $report->create_selection_fields($this->reportFd);
761
762 $actionContent = $this->handle_report_action($report);
763 if ($actionContent !== '') {
764 return $actionContent;
765 }
766
767 $rwhere = $report->get_fld_val('dbx_rwhere', '', 'sqlsearch|max=64');
768 $rsort = $report->get_fld_val('dbx_rsort', 'title', 'parameter');
769 $rdesc = $report->get_fld_val('dbx_rdesc', 'ASC', 'parameter');
770 $rrows = $report->get_fld_val('dbx_rrows', 20, 'int');
771 $rpos = $report->get_fld_val('dbx_rpos', 0, 'int');
772 $rselect = $report->get_fld_val('dbx_rselect', 0, 'int');
773
774 if ($rwhere !== '') {
775 $rwhere = array('search' => array('value' => $rwhere, 'like' => array('title', 'description'), 'mode' => 'contains'));
776 }
777 if ($rselect) {
778 $rwhere = $report->add_rwhere_select(is_string($rwhere) ? $rwhere : '');
779 }
780
781 $flds = $this->report_fields();
782 $report->_rflds = $flds;
783 $report->_rrows = $rrows;
784 $report->_rpos = $rpos;
785 $report->_count_all = $this->db()->count($this->dd);
786 $report->_rcount = $this->db()->count($this->dd, $rwhere);
787 $report->_rdata = $this->db()->select($this->dd, $rwhere, $flds, $rsort, $rdesc, '', $rrows, $rpos);
788
789 return $report->run();
790 }
791
792 private function handle_report_action($report) {
793 $do = dbx()->get_modul_var('dbx_do', '', 'parameter');
794 $rid = (int)dbx()->get_modul_var('rid', 0, 'int');
795
796 if ($do === 'row_edit' && $rid > 0) {
797 return $this->form($rid);
798 }
799 if (($do === 'row_show' || $do === 'detail') && $rid > 0) {
800 return $this->detail($rid);
801 }
802 if ($do === 'row_delete' && $rid > 0) {
803 $ok = $this->db()->delete($this->dd, $rid);
804 $report->del_selected($rid);
805 $report->_msg_success = $ok ? 'Datensatz geloescht.' : '';
806 $report->_msg_error = $ok ? '' : 'Datensatz konnte nicht geloescht werden.';
807 return '';
808 }
809 if ($do === 'multi_delete') {
810 $result = $report->delete_multi_selected_records($this->dd);
811 $report->apply_multi_delete_result($result);
812 return '';
813 }
814 return '';
815 }
816
817 private function report_fields() {
818 return __FIELDS_EXPORT__;
819 }
820
821 public function report_header_callback($report, $content) {
822 return $content;
823 }
824
825 public function report_page_header_callback($report, $content) {
826 return $content;
827 }
828
829 public function report_table_header_callback($report, $content) {
830 return $content;
831 }
832
833 public function report_body_callback($report, $content) {
834 return $content;
835 }
836
837 public function report_table_footer_callback($report, $content) {
838 return $content;
839 }
840
841 public function report_page_footer_callback($report, $content) {
842 return $content;
843 }
844
845 public function report_footer_callback($report, $content) {
846 return $content;
847 }
848
849 public function report_next_record_callback($report, $record) {
850 if (is_array($record) && isset($record['status'])) {
851 $record['status'] = strtoupper((string)$record['status']);
852 }
853 return $record;
854 }
855
856 public function report_row_action_data_callback($report, $payload) {
857 if (is_array($payload) && isset($payload['data'], $payload['type'])) {
858 if ($payload['type'] === 'show') {
859 $payload['data']['tooltip'] = 'Details anzeigen';
860 }
861 }
862 return $payload;
863 }
864
865 public function form_init_callback($form, $value) {
866 return $value;
867 }
868
869 public function form_submit_callback($form, $value) {
870 return $value;
871 }
872
873 public function form_run_callback($form, $content) {
874 return $content;
875 }
876
877 public function api() {
878 dbx()->json_response(array('ok' => 1, 'module' => '__MODUL__', 'dd' => $this->dd));
879 }
880}
881PHP;
882
883 return str_replace(
884 array('__MODUL__', '__CLASS__', '__DD_REF__', '__FORM_FD__', '__REPORT_FD__', '__FIELDS_EXPORT__'),
885 array($modul, $class, var_export($ddRef, true), var_export($formFd, true), var_export($reportFd, true), $fieldsExport),
887 );
888 }
889
890 private function generate_result_template(array $in) {
891 return '<div class="p-3"><h2>' . $this->esc($in['title']) . '</h2><p>{content}</p></div>';
892 }
893
894 private function generate_module_readme(array $in) {
895 $modul = $in['xmodul'];
896 $dd = $in['dd_name'];
897 $template = <<<'MD'
898# __MODUL__ Modul-Wizard Dokumentation
899
900Diese Datei wurde vom dbxAdmin Modul-Wizard erzeugt. Sie ist absichtlich knapp und technisch, damit Mensch und KI die Struktur direkt verstehen.
901
902## Grenzen
903
904- Modulwurzel: `dbx/modules/__MODUL__/`
905- KI darf nur innerhalb dieser Modulwurzel arbeiten.
906- DD-Dateien muessen vollstaendig sein, keine DD-Includes.
907- Templates werden in PHP ueber `dbx()->get_system_obj('dbxTPL')->get_tpl('__MODUL__|template', $data)` gerendert.
908- DD->DB Sync nur fuer `__MODUL__|__DD__`.
909
910## Dateien
911
912- `__MODUL__.class.php`: Einstieg und Routing nach `dbx_run1`.
913- `include/__MODUL__Service.class.php`: Formular, Report, Detail, API und Callbacks.
914- `dd/__DD__.dd.php`: Data Dictionary und DB-Struktur.
915- `fd/__DD__-form.fd.php`: Formularfelder.
916- `fd/rpt-__DD__-selection.fd.php`: Report-Filter und Report-Auswahl.
917- `tpl/htm/start.htm`: Start-Template.
918
919## Formular
920
921Route: `?dbx_modul=__MODUL__&dbx_run1=form`
922
923Enthaltene Muster:
924
925- `save_post()` fuer Insert/Update.
926- `dbx_do=delete` fuer Delete.
927- `form_action_buttons()` fuer Save/Delete/Report Buttons.
928- `configure_form_fields()` als zentraler Ort fuer Felder.
929- Form-Callbacks:
930 - `form_init_callback($form, $value)`
931 - `form_submit_callback($form, $value)`
932 - `form_run_callback($form, $content)`
933
934Gängige Feldtemplates:
935
936- `text-label`: einzeiliger Text.
937- `textarea-label`: mehrzeiliger Text, auch fuer HTML-Felder geeignet wenn Regeln es zulassen.
938- `checkbox-label`: 0/1 Checkbox.
939- `select-single-label`: einfache Auswahl mit `options`.
940- `select-multible-label` oder `multiselect2`: Mehrfachauswahl.
941- `date-label`: Datum.
942- `integer-label`: Zahlen.
943- `password-label`: Passwort.
944- `hidden`: verstecktes Feld.
945
946## Report
947
948Route: `?dbx_modul=__MODUL__&dbx_run1=report`
949
950Enthaltene Muster:
951
952- Multi-Select ueber dbxReport Remember.
953- Multi-Delete ueber `delete_multi_selected_records()`.
954- Row-Edit ueber `dbx_do=row_edit`.
955- Row-Detail ueber `dbx_do=row_show` oder `dbx_do=detail`.
956- Row-Delete ueber `dbx_do=row_delete`.
957- Filter `dbx_rselect=1` zeigt nur ausgewaehlte IDs.
958
959Report-Callbacks:
960
961- `report_header_callback($report, $content)`
962- `report_page_header_callback($report, $content)`
963- `report_table_header_callback($report, $content)`
964- `report_body_callback($report, $content)`
965- `report_table_footer_callback($report, $content)`
966- `report_page_footer_callback($report, $content)`
967- `report_footer_callback($report, $content)`
968- `report_next_record_callback($report, $record)`
969- `report_row_action_data_callback($report, $payload)`
970
971## API
972
973Route: `?dbx_modul=__MODUL__&dbx_run1=api`
974
975Die API-Methode ist ein Platzhalter und liefert JSON. Erweiterungen bleiben im Modul.
976MD;
977
978 return str_replace(array('__MODUL__', '__DD__'), array($modul, $dd), $template);
979 }
980
981 private function generate_module(array $in, array &$log) {
982 $modul = $in['xmodul'];
983 $ok = $this->ensure_dirs($modul, $log);
984 $overwrite = (int)$in['overwrite'] === 1;
985 $write = function($rel, $content) use ($modul, $overwrite, $in, &$log, &$ok) {
986 $written = $this->write_module_file($modul, $rel, $content, $overwrite || $in['target_mode'] === 'new', $log);
987 $ok = $written && $ok;
988 };
989
990 if ((int)$in['backup'] === 1 && $in['target_mode'] === 'existing') {
991 $backup = $this->backup_module($modul);
992 if ($backup) {
993 $log[] = array('type' => 'ok', 'text' => 'Backup erstellt: ' . $backup);
994 $restore = '?dbx_modul=dbxAdmin&dbx_run1=modules&dbx_run2=modul_new&dbx_run3=restore_backup&xmodul=' . rawurlencode($modul) . '&file=' . rawurlencode($backup);
995 $log[] = array('type' => 'link', 'text' => 'Restore-Link: ' . $restore);
996 } else {
997 $log[] = array('type' => 'skip', 'text' => 'Backup nicht erstellt (ZipArchive nicht verfuegbar oder Modul leer).');
998 }
999 }
1000
1001 $config = "<?php\n\$config['version']='1.0';\n\$config['activ']='1';\n\$config['dbxConfig_modul']='secure';\n\$config['groups']='admin';\n\$config['title']=" . var_export($in['title'], true) . ";\n\$config['default_run1']=" . var_export($in['default_run1'], true) . ";\n\$config['default_run2']=" . var_export($in['default_run2'], true) . ";\n";
1002 $write('cfg/config.php', $config);
1003 $write($modul . '.class.php', $this->generate_main_class($in));
1004
1005 if ((int)$in['create_include'] === 1) {
1006 $write('include/' . $modul . 'Service.class.php', $this->generate_service_class($in));
1007 }
1008 $write('README-MODUL-WIZARD.md', $this->generate_module_readme($in));
1009 if ($in['dd_mode'] === 'new') {
1010 $write('dd/' . $in['dd_name'] . '.dd.php', $this->generate_dd($in));
1011 }
1012 if ((int)$in['create_form'] === 1 && $in['dd_mode'] !== 'none') {
1013 $write('fd/' . $in['dd_name'] . '-form.fd.php', $this->generate_form_fd($in));
1014 }
1015 if ((int)$in['create_report'] === 1 && $in['dd_mode'] !== 'none') {
1016 $write('fd/rpt-' . $in['dd_name'] . '-selection.fd.php', $this->generate_report_fd());
1017 }
1018 if ((int)$in['create_templates'] === 1) {
1019 $write('tpl/htm/modul-help.htm', '<p>' . $this->esc($in['title']) . '</p>');
1020 $write('tpl/htm/start.htm', $this->generate_result_template($in));
1021 if ((int)$in['create_form'] === 1) {
1022 $write('tpl/htm/' . strtolower($modul) . '-form.htm', $this->generate_form_template());
1023 }
1024 if ((int)$in['create_report'] === 1) {
1025 $write('tpl/htm/' . strtolower($modul) . '-report.htm', $this->generate_report_template());
1026 }
1027 }
1028
1029 if ($in['dd_mode'] !== 'none') {
1030 $syncUrl = '?dbx_modul=dbxAdmin&dbx_run1=dd&dbx_run2=sync_dd_to_db&modul=' . rawurlencode($modul) . '&dd=' . rawurlencode($in['dd_name']) . '&mode=apply&reset=1';
1031 if ($in['sync_mode'] === 'apply') {
1032 $state = $this->sync_dd_to_db($modul, $in['dd_name']);
1033 $syncOk = (($state['status'] ?? '') === 'finished');
1034 $log[] = array('type' => $syncOk ? 'ok' : 'error', 'text' => 'DD->DB Sync: ' . (string)($state['message'] ?? ($state['status'] ?? '')));
1035 $ok = $syncOk && $ok;
1036 } elseif ($in['sync_mode'] === 'link') {
1037 $log[] = array('type' => 'link', 'text' => 'DD->DB Sync: ' . $syncUrl);
1038 }
1039 }
1040
1041 $startUrl = '?dbx_modul=' . rawurlencode($modul) . '&dbx_run1=' . rawurlencode($in['default_run1'] ?: 'run');
1042 $log[] = array('type' => 'link', 'text' => 'Modul starten: ' . $startUrl);
1043 if ((int)$in['create_form'] === 1) {
1044 $log[] = array('type' => 'link', 'text' => 'Formular oeffnen: ?dbx_modul=' . rawurlencode($modul) . '&dbx_run1=form');
1045 }
1046 if ((int)$in['create_report'] === 1) {
1047 $log[] = array('type' => 'link', 'text' => 'Report oeffnen: ?dbx_modul=' . rawurlencode($modul) . '&dbx_run1=report');
1048 }
1049 if ((int)$in['create_include'] === 1) {
1050 $log[] = array('type' => 'link', 'text' => 'API pruefen: ?dbx_modul=' . rawurlencode($modul) . '&dbx_run1=api');
1051 }
1052 if ($in['dd_mode'] !== 'none') {
1053 $log[] = array('type' => 'link', 'text' => 'DD bearbeiten: ?dbx_modul=dbxAdmin&dbx_run1=edit_dd&modul=' . rawurlencode($modul) . '&dd=' . rawurlencode($in['dd_name']));
1054 if ((int)$in['create_form'] === 1) {
1055 $log[] = array('type' => 'link', 'text' => 'Form-FD bearbeiten: ?dbx_modul=dbxAdmin&dbx_run1=edit_fd&modul=' . rawurlencode($modul) . '&fd=' . rawurlencode($in['dd_name'] . '-form'));
1056 }
1057 if ((int)$in['create_report'] === 1) {
1058 $log[] = array('type' => 'link', 'text' => 'Report-FD bearbeiten: ?dbx_modul=dbxAdmin&dbx_run1=edit_fd&modul=' . rawurlencode($modul) . '&fd=' . rawurlencode('rpt-' . $in['dd_name'] . '-selection'));
1059 }
1060 }
1061
1062 if ((int)$in['ki_package'] === 1) {
1063 $kiUrl = '?dbx_modul=dbxKi&dbx_run1=briefing_module&xmodul=' . rawurlencode($modul) . '&dd_name=' . rawurlencode($in['dd_name']);
1064 $apiUrl = '?dbx_modul=dbxKi&dbx_run1=module_api&action=module.describe&xmodul=' . rawurlencode($modul);
1065 $log[] = array('type' => 'link', 'text' => 'KI-Modulauftrag in dbxKi: ' . $kiUrl);
1066 $log[] = array('type' => 'link', 'text' => 'dbxKi Modul-API: ' . $apiUrl);
1067 }
1068 return $ok;
1069 }
1070
1071 private function sync_dd_to_db($modul, $dd) {
1072 $oDD = dbx()->get_system_obj('dbxDD');
1073 $oDD->sync_dd_to_db($modul, $dd, 'reset');
1074 $state = array('status' => 'running', 'message' => '');
1075 for ($i = 0; $i < 20; $i++) {
1076 $state = $oDD->sync_dd_to_db($modul, $dd, 'apply');
1077 if (in_array((string)($state['status'] ?? ''), array('finished', 'error', 'cancelled'), true)) {
1078 break;
1079 }
1080 }
1081 return is_array($state) ? $state : array('status' => 'error', 'message' => 'Sync ohne Status');
1082 }
1083
1084 private function log_html(array $log) {
1085 if (!$log) return '';
1086 $html = '<div class="dbx-admin-dashboard-panel mt-3"><div class="p-3"><h3>Ergebnis</h3><ul class="list-unstyled mb-0">';
1087 foreach ($log as $row) {
1088 $type = (string)($row['type'] ?? 'info');
1089 $text = (string)($row['text'] ?? '');
1090 $class = $type === 'error' ? 'text-danger' : ($type === 'skip' ? 'text-muted' : 'text-success');
1091 if ($type === 'link' && strpos($text, ': ?') !== false) {
1092 list($label, $url) = explode(': ', $text, 2);
1093 $html .= '<li class="' . $class . '"><a href="' . $this->esc($url) . '">' . $this->esc($label) . '</a></li>';
1094 } else {
1095 $html .= '<li class="' . $class . '">' . $this->esc($text) . '</li>';
1096 }
1097 }
1098 return $html . '</ul></div></div>';
1099 }
1100
1101 private function ki_api() {
1102 $modul = dbx()->get_modul_var('xmodul', '', 'parameter');
1103 $dd = dbx()->get_modul_var('dd_name', '', 'parameter');
1104 if (!$this->valid_name($modul)) {
1105 dbx()->json_response(array('ok' => 0, 'error' => 'ungueltiges Modul'), true);
1106 }
1107 dbx()->json_response($this->ki_manifest($modul, $dd), true);
1108 }
1109
1110 private function ki_manifest($modul, $dd) {
1111 return array(
1112 'ok' => 1,
1113 'bundle_version' => 'module-wizard.v1',
1114 'area' => 'module',
1115 'module' => $modul,
1116 'dd' => $dd,
1117 'root' => 'dbx/modules/' . $modul . '/',
1118 'hard_rules' => array(
1119 'Nur Dateien unter dbx/modules/' . $modul . '/ bearbeiten.',
1120 'Keine globalen Configs, keine anderen Module, keine Dateien in dbx/include aendern.',
1121 'Templates ueber dbxapp-Template-APIs lesen/rendern, z.B. dbx()->get_system_obj(\'dbxTPL\')->get_tpl(...).',
1122 'Template-Aenderungen nur als Modul-Template-Aktion liefern; keine freien Schreibzugriffe ausserhalb von dbx/modules/' . $modul . '/tpl/.',
1123 'Vor destruktiven Aenderungen ein Modul-ZIP erzeugen.',
1124 'Antwort als ZIP mit manifest.json und job.json liefern.',
1125 ),
1126 'allowed_actions' => array(
1127 'module.file.write' => 'Datei unter dem Modul schreiben',
1128 'module.file.delete' => 'Datei unter dem Modul entfernen',
1129 'module.dd.write' => 'DD unter dd/*.dd.php schreiben',
1130 'module.dd.sync' => 'DD->DB Sync fuer Modul-DD anfordern',
1131 'module.template.get' => 'Template ueber dbxTPL/get_tpl lesen oder rendern',
1132 'module.template.set' => 'Template unter tpl/* ueber die dbxapp-Template-Mechanik schreiben',
1133 'module.php.write' => 'PHP unter Modulroot oder include schreiben',
1134 'module.form.extend' => 'Formular mit Save/Delete/Feldtemplates erweitern',
1135 'module.report.extend' => 'Report mit Callbacks, Multi-Select, Multi-Delete, Edit und Detail erweitern',
1136 ),
1137 );
1138 }
1139
1140 private function send_ki_zip() {
1141 if (!class_exists('\\ZipArchive')) {
1142 dbx()->json_response(array('ok' => 0, 'error' => 'ZipArchive nicht verfuegbar.'), true);
1143 }
1144 $modul = dbx()->get_modul_var('xmodul', '', 'parameter');
1145 $dd = dbx()->get_modul_var('dd_name', '', 'parameter');
1146 if (!$this->valid_name($modul)) {
1147 dbx()->json_response(array('ok' => 0, 'error' => 'ungueltiges Modul'), true);
1148 }
1149 $manifest = $this->ki_manifest($modul, $dd);
1150 $job = array(
1151 'job_version' => 'module-wizard.v1',
1152 'module' => $modul,
1153 'steps' => array(
1154 array('id' => 'backup', 'action' => 'module.backup', 'params' => array('module' => $modul)),
1155 array('id' => 'plan', 'action' => 'module.plan', 'params' => array('module' => $modul, 'dd' => $dd)),
1156 ),
1157 );
1158 $auftrag = "# KI-Auftrag Modulprogrammierung\n\n"
1159 . "Arbeite ausschliesslich im Modul `dbx/modules/$modul/`.\n\n"
1160 . "## Harte Regeln\n\n"
1161 . "- Keine Aenderungen ausserhalb von `dbx/modules/$modul/`.\n"
1162 . "- Kein Umbau von Core, dbx/include, globaler config.php oder anderen Modulen.\n"
1163 . "- DD-Dateien vollstaendig schreiben, keine include-basierten DDs.\n"
1164 . "- Templates ueber die dbxapp-Template-Schicht lesen/rendern, z.B. `dbx()->get_system_obj('dbxTPL')->get_tpl(...)`.\n"
1165 . "- Template-Aenderungen als Modul-Template-Set-Aktion liefern; keine Pfade ausserhalb von `dbx/modules/$modul/tpl/`.\n"
1166 . "- Formular-Muster mit Save/Delete, Feldtemplates und Callback-Kommentaren erhalten oder sauber erweitern.\n"
1167 . "- Report-Muster mit Multi-Select, Multi-Delete, Edit, Detail und Callbacks erhalten oder sauber erweitern.\n"
1168 . "- README-MODUL-WIZARD.md aktualisieren, wenn Formular-, Report-, DD- oder API-Verhalten geaendert wird.\n"
1169 . "- Wenn DB-Sync noetig ist, nur fuer `$modul|$dd` anfordern.\n"
1170 . "- Liefere `antwort.zip` mit `manifest.json`, `job.json`, geaenderten Dateien und README.\n\n"
1171 . "## Aufgabe\n\nBeschreibe im Chat die gewuenschte Modulaufgabe und nutze diese ZIP als Arbeitskontext.\n";
1172 $tmp = tempnam(sys_get_temp_dir(), 'dbxmodki');
1173 $zip = new \ZipArchive();
1174 $zip->open($tmp, \ZipArchive::OVERWRITE);
1175 $zip->addFromString('manifest.json', json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
1176 $zip->addFromString('job.vorlage.json', json_encode($job, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
1177 $zip->addFromString('KI-AUFTRAG.md', $auftrag);
1178 $zip->addFromString('README.md', "Modul-KI-Auftrag fuer $modul.\n");
1179 $zip->close();
1180 $name = 'dbxki-modul-' . preg_replace('/[^A-Za-z0-9_-]+/', '-', $modul) . '.zip';
1181 header('Content-Type: application/zip');
1182 header('Content-Disposition: attachment; filename="' . $name . '"');
1183 header('Content-Length: ' . filesize($tmp));
1184 readfile($tmp);
1185 @unlink($tmp);
1186 exit;
1187 }
1188
1189 public function new_modul() {
1190 $in = $this->collect_input();
1191 $log = array();
1192 $oForm = dbx()->get_system_obj('dbxForm');
1193 $oForm->init('form-wizzard-new');
1194 $oForm->_fd = 'dbxAdmin|module-wizard';
1195 $oForm->load_fd_messages();
1196 $oForm->_action = '?dbx_modul=dbxAdmin&dbx_run1=modules&dbx_run2=modul_new';
1197 $oForm->_data = array_merge(array(
1198 'target_mode' => 'new',
1199 'module_template' => 'form_report',
1200 'dd_mode' => 'new',
1201 'field_preset' => 'basic',
1202 'create_include' => 1,
1203 'create_form' => 1,
1204 'create_report' => 1,
1205 'create_templates' => 1,
1206 'overwrite' => 0,
1207 'backup' => 1,
1208 'sync_mode' => 'link',
1209 'ki_package' => 1,
1210 ), $in);
1211 $oForm->add_rep('bar_title', $oForm->get_fd_message('bar_title'));
1212 $oForm->add_rep('bar_subtitle', $oForm->get_fd_message('bar_subtitle'));
1213 $oForm->add_obj(
1214 'bar_actions',
1215 'obj-value',
1216 '<button class="btn btn-primary btn-sm" type="submit"><i class="bi bi-magic"></i> '
1217 . $this->esc($oForm->get_fd_message('action_create')) . '</button>'
1218 );
1219 $oForm->_msg_info = $oForm->get_fd_message('wizard_info');
1220
1221 $oForm->add_fld('target_mode', 'select-single-label', label: $oForm->get_fd_message('label_target'), rules: 'parameter', options: $this->options(array('new' => $oForm->get_fd_message('option_target_new'), 'existing' => $oForm->get_fd_message('option_target_existing'))));
1222 $oForm->add_fld('existing_modul', 'select-single-label', label: $oForm->get_fd_message('label_existing_module'), rules: 'parameter', options: $this->options($this->module_options(true)));
1223 $oForm->add_fld('xmodul', 'text-label', label: $oForm->get_fd_message('label_new_module'), rules: 'parameter|max=63', tooltip: $oForm->get_fd_message('tooltip_module_name'));
1224 $oForm->add_fld('title', 'text-label', label: $oForm->get_fd_message('label_title'), rules: 'varchar|max=120');
1225 $oForm->add_fld('module_template', 'select-single-label', label: $oForm->get_fd_message('label_template'), rules: 'parameter', options: $this->options(array('form_report' => $oForm->get_fd_message('option_template_form_report'), 'form' => $oForm->get_fd_message('option_template_form'), 'report' => $oForm->get_fd_message('option_template_report'), 'api' => $oForm->get_fd_message('option_template_api'), 'blank' => $oForm->get_fd_message('option_template_blank'))));
1226 $oForm->add_fld('default_run1', 'text-label', label: $oForm->get_fd_message('label_run1'), rules: 'parameter|max=32');
1227 $oForm->add_fld('default_run2', 'text-label', label: $oForm->get_fd_message('label_run2'), rules: 'parameter|max=32');
1228 $oForm->add_fld('dd_mode', 'select-single-label', label: $oForm->get_fd_message('label_dd'), rules: 'parameter', options: $this->options(array('new' => $oForm->get_fd_message('option_dd_new'), 'existing' => $oForm->get_fd_message('option_dd_existing'), 'none' => $oForm->get_fd_message('option_dd_none'))));
1229 $oForm->add_fld('dd_ref', 'select-single-label', label: $oForm->get_fd_message('label_existing_dd'), rules: 'parameter', options: $this->options($this->dd_options(true)));
1230 $oForm->add_fld('dd_name', 'text-label', label: $oForm->get_fd_message('label_dd_name'), rules: 'parameter|max=63');
1231 $oForm->add_fld('table_name', 'text-label', label: $oForm->get_fd_message('label_table'), rules: 'parameter|max=63');
1232 $oForm->add_fld('db_file', 'text-label', label: $oForm->get_fd_message('label_db_file'), rules: 'parameter+.-_|max=80');
1233 $oForm->add_fld('field_preset', 'select-single-label', label: $oForm->get_fd_message('label_fields'), rules: 'parameter', options: $this->options(array('basic' => $oForm->get_fd_message('option_fields_basic'), 'content' => $oForm->get_fd_message('option_fields_content'), 'status' => $oForm->get_fd_message('option_fields_status'), 'workflow' => $oForm->get_fd_message('option_fields_workflow'))));
1234 $oForm->add_fld('create_include', 'checkbox-label', label: $oForm->get_fd_message('label_create_include'), rules: 'int');
1235 $oForm->add_fld('create_form', 'checkbox-label', label: $oForm->get_fd_message('label_create_form'), rules: 'int');
1236 $oForm->add_fld('create_report', 'checkbox-label', label: $oForm->get_fd_message('label_create_report'), rules: 'int');
1237 $oForm->add_fld('create_templates', 'checkbox-label', label: $oForm->get_fd_message('label_create_templates'), rules: 'int');
1238 $oForm->add_fld('overwrite', 'checkbox-label', label: $oForm->get_fd_message('label_overwrite'), rules: 'int');
1239 $oForm->add_fld('backup', 'checkbox-label', label: $oForm->get_fd_message('label_backup'), rules: 'int');
1240 $oForm->add_fld('sync_mode', 'select-single-label', label: $oForm->get_fd_message('label_sync'), rules: 'parameter', options: $this->options(array('link' => $oForm->get_fd_message('option_sync_link'), 'none' => $oForm->get_fd_message('option_sync_none'), 'apply' => $oForm->get_fd_message('option_sync_apply'))));
1241 $oForm->add_fld('ki_package', 'checkbox-label', label: $oForm->get_fd_message('label_ki_package'), rules: 'int');
1242 $oForm->add_js_code(<<<'JS'
1243(function () {
1244 var byName = function (name) { return document.querySelector('[name="' + name + '"]'); };
1245 var modul = byName('xmodul');
1246 var dd = byName('dd_name');
1247 var table = byName('table_name');
1248 var dbFile = byName('db_file');
1249 var template = byName('module_template');
1250 var previousModule = '';
1251
1252 var syncNames = function () {
1253 if (!modul || !dd || !table || !dbFile) return;
1254 var name = String(modul.value || '').trim();
1255 var oldDd = previousModule ? previousModule + 'Data' : '';
1256 var oldDb = previousModule ? previousModule + '.db3' : 'modul.db3';
1257 if (!dd.value || dd.value === oldDd) dd.value = name ? name + 'Data' : '';
1258 if (!table.value || table.value === oldDd) table.value = name ? name + 'Data' : '';
1259 if (!dbFile.value || dbFile.value === oldDb || dbFile.value === 'modul.db3') dbFile.value = name ? name + '.db3' : 'modul.db3';
1260 previousModule = name;
1261 };
1262
1263 var applyTemplate = function () {
1264 if (!template) return;
1265 var presets = {
1266 form_report: [1, 1, 1],
1267 form: [1, 1, 0],
1268 report: [1, 0, 1],
1269 api: [1, 0, 0],
1270 blank: [0, 0, 0]
1271 };
1272 var preset = presets[template.value];
1273 if (!preset) return;
1274 ['create_include', 'create_form', 'create_report'].forEach(function (name, index) {
1275 var checkbox = byName(name);
1276 if (checkbox) checkbox.checked = preset[index] === 1;
1277 });
1278 };
1279
1280 if (modul) {
1281 modul.addEventListener('input', syncNames);
1282 syncNames();
1283 }
1284 if (template) template.addEventListener('change', applyTemplate);
1285})();
1286JS
1287 );
1288
1289 if ($oForm->submit()) {
1290 $in = $this->collect_input($oForm);
1291 $errors = array();
1292 if ($this->validate_input($in, $errors) && !$oForm->errors()) {
1293 if ($this->generate_module($in, $log)) {
1294 $oForm->_msg_success = $oForm->get_fd_message('wizard_success');
1295 } else {
1296 $oForm->_msg_error = $oForm->get_fd_message('wizard_error');
1297 }
1298 } else {
1299 foreach ($errors as $err) {
1300 $oForm->add_fld_error('xmodul', $err);
1301 }
1302 $oForm->_msg_error = $oForm->get_fd_message('check_input');
1303 }
1304 }
1305
1306 return $oForm->run() . $this->log_html($log);
1307 }
1308
1309 public function run($run = '') {
1310 $run3 = dbx()->get_modul_var('dbx_run3', '', 'parameter');
1311 if ($run3 === 'restore_backup') {
1312 return $this->restore_module_backup();
1313 }
1314 if ($run3 === 'ki_zip') {
1315 $this->send_ki_zip();
1316 }
1317 if ($run3 === 'ki_api') {
1318 $this->ki_api();
1319 }
1320 return $this->new_modul();
1321 }
1322}
$table['server']
Definition .dd.php:6
$indexes[]
Definition .dd.php:167
$messages
Definition config.fd.php:2
if((string)($formActionPolicy['action'] ?? '') !=='dbxAction.save'||!dbx() ->check_action_token((string)($formActionPolicy['scope'] ?? ''),(string)($formActionRoute['params']['dbx_token'] ?? ''))) $report
os_path(string $path)
Normalisiert einen Pfad fuer das aktuelle Betriebssystem.
Definition dbxApi.php:1538
get_base_dir(int $cutData=0)
Liefert das Basisverzeichnis der Installation.
Definition dbxApi.php:1507
get_file_dir()
Liefert das files/-Verzeichnis der Installation.
Definition dbxApi.php:1524
if($resolved !==$expectedBase . 'files/test/') $config
if($web->content_permalink_redirect_target() !=='') $tpl
if(PHP_SAPI !=='cli') $write
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.