14 private const SESSION_KEY =
'design_jobs';
15 private const RESULT_CONTRACT =
'dbx.design.result.v1';
18 private $designService;
22 $this->designService =
dbx()->get_include_obj(
'dbxDesignService',
'dbxDesign_admin');
25 private function h(
$value): string {
26 return htmlspecialchars((string)
$value, ENT_QUOTES,
'UTF-8');
29 private function texts() {
33 dbx()->get_system_obj(
'dbxForm',
'use');
35 $texts->init(
'ki-design-texts');
36 $texts->_fd =
'dbxKi|design-briefing';
37 $texts->load_fd_messages();
38 $texts->set_form_help_enabled(
false);
43 private function message(
string $key, array $replacements = array()): string {
45 ? $this->texts()->format_fd_message($key, $replacements)
46 : $this->texts()->get_fd_message($key);
49 private function url(
string $run1, array $params = array()): string {
50 $url =
'?dbx_modul=dbxKi&dbx_run1=' . rawurlencode($run1);
51 foreach ($params as $key =>
$value) {
53 $url .=
'&' . rawurlencode((
string)$key) .
'=' . rawurlencode((
string)
$value);
59 private function designOptions(): array {
61 foreach ($this->designService->listDesigns() as $name => $meta) {
62 $items[$name] = $meta[
'title'] .
' (' . $name .
')';
67 private function briefingForm(
string $template =
'ki-design-briefing') {
68 $form =
dbx()->get_system_obj(
'dbxForm');
70 $form->_fd =
'dbxKi|design-briefing';
71 $form->load_fd_messages();
72 $form->set_form_help_enabled(
false);
73 $form->_action = $this->url(
'briefing_design_export');
74 $form->_msg_info =
'';
75 $selected = $this->designService->normalizeName((
string)
dbx()->get_request_var(
'design',
'dbxapp',
'parameter'));
76 if (!isset($this->designService->listDesigns()[$selected])) {
79 $mode = strtolower((
string)
dbx()->get_request_var(
'mode',
'update',
'parameter')) ===
'create' ?
'create' :
'update';
80 $form->_data = array_merge(
$form->_data, array(
82 'source_design' => $selected,
83 'target_design' =>
$mode ===
'update' ? $selected :
'',
87 'branding_notes' =>
'',
89 'responsive_notes' =>
'',
90 'preserve_behavior' => 1,
93 $form->add_fld(
'mode',
'select-single-label', label:
$texts->get_fd_message(
'label_mode'), rules:
'parameter', options: array(
94 'update' =>
$texts->get_fd_message(
'mode_update'),
95 'create' =>
$texts->get_fd_message(
'mode_create'),
97 $form->add_fld(
'source_design',
'select-single-label', label:
$texts->get_fd_message(
'label_source_design'), rules:
'parameter|max=63', options: $this->designOptions(), dd:
'');
98 $form->add_fld(
'target_design',
'text-label', label:
$texts->get_fd_message(
'label_target_design'), rules:
'parameter+_-|min=2|max=63', placeholder:
$texts->get_fd_message(
'placeholder_target_design'), dd:
'');
99 $form->add_fld(
'goal',
'textarea-label', label:
$texts->get_fd_message(
'label_goal'), rules:
'varchar|min=10|max=6000', data:
'rows=7', placeholder:
$texts->get_fd_message(
'placeholder_goal'), dd:
'');
100 $form->add_fld(
'layout_notes',
'textarea-label', label:
$texts->get_fd_message(
'label_layout_notes'), rules:
'varchar|max=1800', data:
'rows=3', placeholder:
$texts->get_fd_message(
'placeholder_layout'), dd:
'');
101 $form->add_fld(
'menu_notes',
'textarea-label', label:
$texts->get_fd_message(
'label_menu_notes'), rules:
'varchar|max=1800', data:
'rows=3', placeholder:
$texts->get_fd_message(
'placeholder_menu'), dd:
'');
102 $form->add_fld(
'branding_notes',
'textarea-label', label:
$texts->get_fd_message(
'label_branding_notes'), rules:
'varchar|max=1800', data:
'rows=3', placeholder:
$texts->get_fd_message(
'placeholder_branding'), dd:
'');
103 $form->add_fld(
'footer_notes',
'textarea-label', label:
$texts->get_fd_message(
'label_footer_notes'), rules:
'varchar|max=1800', data:
'rows=3', placeholder:
$texts->get_fd_message(
'placeholder_footer'), dd:
'');
104 $form->add_fld(
'responsive_notes',
'textarea-label', label:
$texts->get_fd_message(
'label_responsive_notes'), rules:
'varchar|max=1800', data:
'rows=3', placeholder:
$texts->get_fd_message(
'placeholder_responsive'), dd:
'');
105 $form->add_fld(
'preserve_behavior',
'checkbox-label', label:
$texts->get_fd_message(
'label_preserve_behavior'), rules:
'int', dd:
'');
117 $form = $this->briefingForm();
118 $form->add_rep(
'design_list_url', $this->h(
'?dbx_modul=dbxDesign_admin&dbx_run1=list'));
119 $form->add_rep(
'import_panel', $importPanel);
120 $form->add_rep(
'bar_title', $this->message(
'bar_title'));
121 $form->add_rep(
'bar_subtitle', $this->message(
'bar_subtitle'));
122 $form->add_rep(
'bar_icon',
'bi-stars');
123 $form->add_rep(
'bar_actions',
'<a class="btn btn-outline-secondary btn-sm" href="' . $this->h(
'?dbx_modul=dbxDesign_admin&dbx_run1=list') .
'"><i class="bi bi-palette"></i> ' . $this->h($this->message(
'action_design_studio')) .
'</a>');
124 foreach ($this->barDefaults() as $key =>
$value) {
134 $form = $this->briefingForm();
135 if (!
$form->submit()) {
136 throw new \RuntimeException($this->message(
'token_error'));
138 $in = $this->collectBriefing(
$form);
139 $sourceDir = $this->designService->designDir($in[
'source_design']);
140 if (!is_dir($sourceDir)) {
141 throw new \RuntimeException($this->message(
'source_not_found'));
143 if ($in[
'mode'] ===
'update') {
144 $in[
'target_design'] = $in[
'source_design'];
145 } elseif (!$this->designService->isValidName($in[
'target_design'])) {
146 throw new \InvalidArgumentException($this->message(
'target_required'));
147 } elseif (isset($this->designService->listDesigns()[$in[
'target_design']])) {
148 throw new \InvalidArgumentException($this->message(
'target_exists'));
150 if (trim($in[
'goal']) ===
'') {
151 throw new \InvalidArgumentException($this->message(
'goal_required'));
153 if (!class_exists(
'ZipArchive')) {
154 throw new \RuntimeException($this->message(
'zip_unavailable'));
157 $tmp = tempnam(sys_get_temp_dir(),
'dbx-design-briefing-');
158 $zip = new \ZipArchive();
159 if (
$tmp ===
false || $zip->open(
$tmp, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !==
true) {
160 throw new \RuntimeException($this->message(
'briefing_zip_error'));
163 'contract' =>
'dbx.design.briefing.v1',
164 'result_contract' => self::RESULT_CONTRACT,
165 'mode' => $in[
'mode'],
166 'source_design' => $in[
'source_design'],
167 'target_design' => $in[
'target_design'],
168 'created_at' => date(DATE_ATOM),
169 'required_result_root' =>
'result/design/',
172 'design' => $this->designService->readMetadata($in[
'source_design']),
173 'validation' => $this->designService->validateDesignDirectory($sourceDir,
false),
174 'files' => array_keys($this->designService->relativeFiles($sourceDir)),
176 'content_slot' =>
'[dbx:content]',
177 'design_slots' => array(
'[dbx:logo]',
'[dbx:branding]',
'[dbx:footer]'),
178 'template_engine' =>
'dbxTPL',
179 'admin_module' =>
'dbxDesign_admin',
182 $zip->addFromString(
'00-START.md', $this->startInstructions($in));
183 $zip->addFromString(
'manifest.json', json_encode(
$manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
184 $zip->addFromString(
'briefing.json', json_encode($in, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
185 $zip->addFromString(
'context.json', json_encode($context, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
186 $zip->addFromString(
'KI-AUFTRAG.md', $this->aiInstructions($in));
187 $zip->addFromString(
'RESULT-FORMAT.md', $this->resultFormat());
188 foreach ($this->designService->relativeFiles($sourceDir) as $relative => $absolute) {
189 $zip->addFile($absolute,
'source-design/' . $in[
'source_design'] .
'/' . $relative);
192 $name =
'dbxki-design-' . $in[
'mode'] .
'-' . $in[
'target_design'] .
'.zip';
193 header(
'Content-Type: application/zip');
194 header(
'Content-Disposition: attachment; filename="' . $name .
'"');
195 header(
'Content-Length: ' . filesize(
$tmp));
201 private function collectBriefing(
$form): array {
203 'mode' =>
'parameter',
204 'source_design' =>
'parameter|max=63',
205 'target_design' =>
'parameter+_-|max=63',
206 'goal' =>
'varchar|max=6000',
207 'layout_notes' =>
'varchar|max=1800',
208 'menu_notes' =>
'varchar|max=1800',
209 'branding_notes' =>
'varchar|max=1800',
210 'footer_notes' =>
'varchar|max=1800',
211 'responsive_notes' =>
'varchar|max=1800',
212 'preserve_behavior' =>
'int',
218 $out[
'mode'] =
$out[
'mode'] ===
'create' ?
'create' :
'update';
219 $out[
'source_design'] = $this->designService->normalizeName(
$out[
'source_design']);
220 $out[
'target_design'] = $this->designService->normalizeName(
$out[
'target_design']);
224 private function startInstructions(array $in): string {
225 return
"# dbxKi Design-Auftrag\n\n"
226 .
"1. Lies `KI-AUFTRAG.md`, `briefing.json`, `context.json` und das komplette Ausgangsdesign.\n"
227 .
"2. Bearbeite ausschließlich Design-Dateien. Keine Fachlogik, Datenbank, Rechte oder Module ändern.\n"
228 .
"3. Lege nur neue/geänderte Dateien unter `result/design/` ab.\n"
229 .
"4. Erzeuge `manifest.json` im Antwort-ZIP nach `RESULT-FORMAT.md`.\n"
230 .
"5. Ergebnis als ZIP zurückgeben. Ziel: `" . $in[
'target_design'] .
"`.\n";
233 private function aiInstructions(array $in): string {
234 return
"# Verbindlicher Design-Auftrag\n\n"
235 .
"## Ziel\n\n" . $in[
'goal'] .
"\n\n"
236 .
"## Aufteilung\n\n" . ($in[
'layout_notes'] ?:
'(aus dem Ziel ableiten)') .
"\n\n"
237 .
"## Menü\n\n" . ($in[
'menu_notes'] ?:
'(bestehende Navigation kompatibel weiterverwenden)') .
"\n\n"
238 .
"## Branding\n\n" . ($in[
'branding_notes'] ?:
'(bestehendes Branding behutsam weiterentwickeln)') .
"\n\n"
239 .
"## Footer\n\n" . ($in[
'footer_notes'] ?:
'(Fenster-Dock und Rechtelinks funktionsfähig erhalten)') .
"\n\n"
240 .
"## Mobil / Barrierefreiheit\n\n" . ($in[
'responsive_notes'] ?:
'Responsive, tastaturbedienbar und kontrastreich umsetzen.') .
"\n\n"
241 .
"## Nicht verhandelbar\n\n"
242 .
"- `[dbx:content]` genau einmal erhalten.\n"
243 .
"- `{dbx:title}`, `{dbx:design}`, `{dbx:skin_css}`, `{dbx:skin_class}` und `core.js?design={dbx:design}` erhalten.\n"
244 .
"- dbxMenu-Aufrufe, Admin-Menü, Ajax, openWin, Fenster-Dock, dbxForm und dbxReport bleiben funktionsfähig.\n"
245 .
"- Optionale Bereiche über `[dbx:logo]`, `[dbx:branding]`, `[dbx:footer]` und die gleichnamigen Dateien unter `htm/` strukturieren.\n"
246 .
"- Keine PHP-Dateien, keine Datenbank, keine DD/FD, keine Module und keine externen Build-Abhängigkeiten liefern.\n"
247 .
"- Das Paket bleibt eigenständig; keine privaten Dateien eines anderen Designs referenzieren.\n";
250 private function resultFormat(): string {
251 return
"# Antwort-ZIP\n\n"
252 .
"```text\nmanifest.json\nresult/design/htm/default.htm\nresult/design/css/design-custom.css\n...\n```\n\n"
253 .
"`manifest.json`:\n\n"
254 .
"```json\n{\n \"contract\": \"" . self::RESULT_CONTRACT .
"\",\n"
255 .
" \"mode\": \"create|update\",\n \"source_design\": \"dbxapp\",\n \"target_design\": \"zielname\",\n"
256 .
" \"summary\": \"Kurze Beschreibung\"\n}\n```\n\n"
257 .
"Bei `update` nur neue/geänderte Dateien liefern. Bei `create` darf ebenfalls ein Delta zum mitgelieferten Ausgangsdesign geliefert werden. dbxapp baut daraus im Staging ein vollständiges Paket und prüft es vor der Freigabe.\n";
260 private function importForm() {
261 $form =
dbx()->get_system_obj(
'dbxForm');
262 $form->init(
'ki-design-result-import',
'ki-design-import');
263 $form->_fd =
'dbxKi|design-briefing';
264 $form->load_fd_messages();
265 $form->set_form_help_enabled(
false);
266 $form->_action = $this->url(
'design_bundle_import');
267 $form->_msg_info =
'';
275 return $this->importForm()->run();
282 $form = $this->importForm();
284 if (!
$form->submit()) {
285 throw new \RuntimeException($this->message(
'token_error'));
287 $upload = is_array($_FILES[
'design_zip'] ??
null) ? $_FILES[
'design_zip'] : array();
288 $state = $this->extractResult($upload);
289 $token = bin2hex(random_bytes(12));
290 if (!isset(
$_SESSION[
'dbx'][
'dbxKi'][self::SESSION_KEY]) || !is_array(
$_SESSION[
'dbx'][
'dbxKi'][self::SESSION_KEY])) {
291 $_SESSION[
'dbx'][
'dbxKi'][self::SESSION_KEY] = array();
295 }
catch (\Throwable $e) {
296 return dbx()->get_system_obj(
'dbxTPL')->get_tpl(
'dbxKi|ki-design-result', array(
297 'result_class' =>
'alert-danger',
298 'result_title' => $this->message(
'import_failed_title'),
299 'result_message' => $this->h($e->getMessage()),
300 'result_actions' =>
'<a class="btn btn-outline-secondary" href="' . $this->h($this->url(
'briefing_design')) .
'">' . $this->h($this->message(
'action_back')) .
'</a>',
305 private function extractResult(array
$file): array {
306 if ((
$file[
'error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
307 throw new \InvalidArgumentException($this->message(
'zip_select'));
309 $tmp = (string)(
$file[
'tmp_name'] ??
'');
310 if (
$tmp ===
'' || !is_uploaded_file(
$tmp)) {
311 throw new \InvalidArgumentException($this->message(
'zip_upload_invalid'));
313 $maxBytes = max(1, (
int)
dbx()->get_config(
'dbxKi',
'max_bundle_bytes', 52428800));
314 if ((
int)(
$file[
'size'] ?? 0) > $maxBytes) {
315 throw new \InvalidArgumentException($this->message(
'zip_too_large'));
317 if (!class_exists(
'ZipArchive')) {
318 throw new \RuntimeException($this->message(
'zip_unavailable'));
321 if (!mkdir(
$work, 0777,
true) && !is_dir(
$work)) {
322 throw new \RuntimeException($this->message(
'staging_error'));
325 $zip = new \ZipArchive();
326 if ($zip->open(
$tmp) !==
true) {
327 throw new \InvalidArgumentException($this->message(
'zip_open_error'));
330 $maxFiles = max(50, (
int)
dbx()->get_config(
'dbxKi',
'max_bundle_files', 50));
331 if ($zip->numFiles > $maxFiles) {
333 throw new \InvalidArgumentException($this->message(
'zip_too_many_files'));
335 for ($i = 0; $i < $zip->numFiles; $i++) {
336 $name = str_replace(
'\\',
'/', (
string)$zip->getNameIndex($i));
337 $stat = $zip->statIndex($i);
338 $total += (int)($stat[
'size'] ?? 0);
339 if ($name ===
'' || $name[0] ===
'/' || strpos($name,
'..') !==
false || strpos($name,
':') !==
false || strpos($name,
"\0") !==
false) {
341 throw new \InvalidArgumentException($this->message(
'zip_path_invalid', array(
'path' => $name)));
343 if ($total > $maxBytes) {
345 throw new \InvalidArgumentException($this->message(
'zip_unpacked_too_large'));
348 if (!$zip->extractTo(
$work)) {
350 throw new \RuntimeException($this->message(
'zip_extract_error'));
355 $resultDir =
$root . DIRECTORY_SEPARATOR .
'result' . DIRECTORY_SEPARATOR .
'design';
357 if (!is_array(
$manifest) || (
$manifest[
'contract'] ??
'') !== self::RESULT_CONTRACT) {
358 throw new \InvalidArgumentException($this->message(
'manifest_contract_error', array(
'contract' => self::RESULT_CONTRACT)));
360 if (!is_dir($resultDir)) {
361 throw new \InvalidArgumentException($this->message(
'result_directory_missing'));
363 $rawMode = strtolower(trim((
string)(
$manifest[
'mode'] ??
'')));
364 if (!in_array($rawMode, array(
'create',
'update'),
true)) {
365 throw new \InvalidArgumentException($this->message(
'manifest_mode_error'));
368 $source = $this->designService->normalizeName((
string)(
$manifest[
'source_design'] ??
''));
369 $target = $this->designService->normalizeName((
string)(
$manifest[
'target_design'] ??
''));
370 if (!$this->designService->isValidName(
$source) || !$this->designService->isValidName(
$target)) {
371 throw new \InvalidArgumentException($this->message(
'manifest_design_error'));
373 $designs = $this->designService->listDesigns();
374 if (!isset($designs[
$source])) {
375 throw new \InvalidArgumentException($this->message(
'manifest_source_missing'));
378 throw new \InvalidArgumentException($this->message(
'manifest_target_exists'));
381 throw new \InvalidArgumentException($this->message(
'manifest_target_missing'));
383 $files = $this->designService->relativeFiles($resultDir);
385 throw new \InvalidArgumentException($this->message(
'result_empty'));
387 foreach (
$files as $relative => $absolute) {
388 $fileError = $this->designService->validateResultFile($relative, $absolute);
389 if ($fileError !==
'') {
390 throw new \InvalidArgumentException($this->message(
'result_file_invalid', array(
'file' => $relative)));
393 }
catch (\Throwable $e) {
394 $this->removeWorkDir(
$work);
399 'result_dir' => $resultDir,
404 'files' => array_keys(
$files),
408 private function findResultRoot(
string $work): string {
409 if (is_file(
$work .
'/manifest.json')) {
412 $children = glob(
$work .
'/*', GLOB_ONLYDIR) ?: array();
413 if (count($children) === 1 && is_file($children[0] .
'/manifest.json')) {
419 private function renderPreview(
string $token, array
$state): string {
420 $targetBase = is_dir($this->designService->designDir(
$state[
'target_design']))
421 ? $this->designService->designDir(
$state[
'target_design'])
422 : $this->designService->designDir(
$state[
'source_design']);
424 foreach (
$state[
'files'] as $relative) {
425 $new =
$state[
'result_dir'] . DIRECTORY_SEPARATOR . str_replace(
'/', DIRECTORY_SEPARATOR, $relative);
426 $old = $targetBase . DIRECTORY_SEPARATOR . str_replace(
'/', DIRECTORY_SEPARATOR, $relative);
427 $statusKey = !is_file($old) ?
'new' : (hash_file(
'sha256', $old) === hash_file(
'sha256', $new) ?
'unchanged' :
'changed');
428 $class = $statusKey ===
'new' ?
'text-bg-success' : ($statusKey ===
'changed' ?
'text-bg-warning' :
'text-bg-secondary');
429 $rows .=
'<tr><td><code>' . $this->h($relative) .
'</code></td><td><span class="badge ' .
$class .
'">' . $this->h($this->message(
'status_' . $statusKey)) .
'</span></td><td class="text-end">' . number_format((
int)filesize($new), 0,
',',
'.') .
' B</td></tr>';
431 $form =
dbx()->get_system_obj(
'dbxForm');
432 $form->init(
'ki-design-result-apply',
'ki-design-preview');
433 $form->_fd =
'dbxKi|design-briefing';
434 $form->load_fd_messages();
435 $form->set_form_help_enabled(
false);
436 $form->_action = $this->url(
'design_bundle_apply');
437 $form->_msg_info =
'';
439 $form->add_fld(
'job_token',
'dbx|hidden', rules:
'parameter', dd:
'');
441 $form->add_rep(
'source_design', $this->h(
$state[
'source_design']));
442 $form->add_rep(
'target_design', $this->h(
$state[
'target_design']));
443 $form->add_rep(
'summary', $this->h((
string)(
$state[
'manifest'][
'summary'] ??
'')));
444 $form->add_rep(
'file_count', count(
$state[
'files']));
446 $form->add_rep(
'back_url', $this->h($this->url(
'briefing_design')));
447 $form->add_rep(
'bar_title', $this->message(
'preview_title'));
448 $form->add_rep(
'bar_subtitle', $this->h(
$state[
'mode'] .
': ' .
$state[
'source_design'] .
' → ' .
$state[
'target_design']));
449 $form->add_rep(
'bar_icon',
'bi-search');
450 $form->add_rep(
'bar_actions',
'');
451 foreach ($this->barDefaults() as $key =>
$value) {
457 private function barDefaults(): array {
459 'bar_class' =>
'dbx-module-bar',
460 'bar_title_class' =>
'dbx-module-bar-titleblock',
461 'bar_title_pre' =>
'',
462 'bar_title_heading_attrs' =>
'',
465 'bar_actions_class' =>
'dbx-module-bar-actions',
473 $form =
dbx()->get_system_obj(
'dbxForm');
474 $form->init(
'ki-design-result-apply',
'ki-design-preview');
475 $form->_fd =
'dbxKi|design-briefing';
476 $form->load_fd_messages();
477 $form->set_form_help_enabled(
false);
478 $form->_action = $this->url(
'design_bundle_apply');
479 $form->_msg_info =
'';
480 $form->add_fld(
'job_token',
'dbx|hidden', rules:
'parameter', dd:
'');
482 if (!
$form->submit()) {
483 throw new \RuntimeException($this->message(
'token_error'));
485 $token = (string)
$form->get_fld_value(
'job_token',
'',
'parameter');
488 throw new \RuntimeException($this->message(
'preview_expired'));
490 $result = $this->designService->applyResult(
496 $this->removeWorkDir((
string)(
$state[
'work_dir'] ??
''));
499 $message = $this->message(
'apply_message', array(
'name' =>
$result[
'name']));
501 $message .=
' ' . $this->message(
'backup_message', array(
'backup' => basename(
$backup)));
503 return dbx()->get_system_obj(
'dbxTPL')->get_tpl(
'dbxKi|ki-design-result', array(
504 'result_class' =>
'alert-success',
505 'result_title' => $this->message(
'apply_success_title'),
506 'result_message' => $this->h($message),
507 'result_actions' =>
'<a class="btn btn-primary" target="_blank" href="' . $this->h(
'?dbx_design=' . rawurlencode(
$result[
'name'])) .
'"><i class="bi bi-eye"></i> ' . $this->h($this->message(
'action_view_design')) .
'</a> '
508 .
'<a class="btn btn-outline-secondary" href="' . $this->h(
'?dbx_modul=dbxDesign_admin&dbx_run1=list') .
'">' . $this->h($this->message(
'action_design_studio')) .
'</a>',
510 }
catch (\Throwable $e) {
511 return dbx()->get_system_obj(
'dbxTPL')->get_tpl(
'dbxKi|ki-design-result', array(
512 'result_class' =>
'alert-danger',
513 'result_title' => $this->message(
'apply_error_title'),
514 'result_message' => $this->h($e->getMessage()),
515 'result_actions' =>
'<a class="btn btn-outline-secondary" href="' . $this->h($this->url(
'briefing_design')) .
'">' . $this->h($this->message(
'action_back')) .
'</a>',
520 private function removeWorkDir(
string $dir): void {
529 $it = new \RecursiveIteratorIterator(
530 new \RecursiveDirectoryIterator(
$dir, \FilesystemIterator::SKIP_DOTS),
531 \RecursiveIteratorIterator::CHILD_FIRST
534 if (
$file->isDir()) {
535 @rmdir(
$file->getPathname());
537 @unlink(
$file->getPathname());