getPathname(), strlen($root) + 1)); if (preg_match($skip, $rel) && !preg_match('#^(backups|uploads|plugins)/(\.htaccess|README\.md)$#', $rel)) continue; if ($f->isDir()) { @mkdir($tmp . '/' . $rel, 0755, true); continue; } @mkdir(dirname($tmp . '/' . $rel), 0755, true); copy($f->getPathname(), $tmp . '/' . $rel); } @mkdir($tmp . '/uploads'); @mkdir($tmp . '/config'); file_put_contents($tmp . '/config/config.php', ' bin2hex(random_bytes(32)), 'db' => ['driver' => 'sqlite', 'sqlite_path' => $tmp . '/database/deskhub.sqlite'], 'site' => ['name' => 'Test', 'url' => 'http://127.0.0.1', 'admin_email' => 'a@test.de', 'timezone' => 'Europe/Berlin'], 'uploads' => ['max_size_bytes' => 10485760, 'allowed_extensions' => ['png'], 'path' => $tmp . '/uploads'], ], true) . ';'); require_once $root . '/includes/db.php'; require_once $root . '/database/migrate.php'; $pdo = new PDO('sqlite:' . $tmp . '/database/deskhub.sqlite'); $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); $pdo->exec('PRAGMA foreign_keys = ON'); $pdo->exec(convert_schema_to_sqlite((string) file_get_contents($root . '/database/schema.sql'))); run_migrations($pdo, true); $now = date('Y-m-d H:i:s'); $pw = password_hash('Sehr-Sicher-1234', PASSWORD_DEFAULT); $pdo->exec('DELETE FROM staff'); $deptA = (int) $pdo->query('SELECT id FROM departments ORDER BY id LIMIT 1')->fetchColumn(); $pdo->exec("INSERT INTO departments (name, email, is_default) VALUES ('Technik', 'b@example.org', 0)"); $deptB = (int) $pdo->query('SELECT MAX(id) FROM departments')->fetchColumn(); $pdo->prepare("INSERT INTO staff (name, email, password_hash, role, department_id, is_active, created_at) VALUES ('Admin','admin@test.de',?,'admin',?,1,?)")->execute([$pw, $deptA, $now]); $prio = (int) $pdo->query('SELECT id FROM priorities ORDER BY id LIMIT 1')->fetchColumn(); $pdo->prepare("INSERT INTO help_topics (name, department_id, priority_id) VALUES ('Allgemein', ?, ?)")->execute([$deptA, $prio]); $topicA = (int) $pdo->lastInsertId(); $pdo->prepare("INSERT INTO help_topics (name, department_id, priority_id) VALUES ('Störung', ?, ?)")->execute([$deptB, $prio]); $topicB = (int) $pdo->lastInsertId(); $pdo->prepare("INSERT INTO custom_fields (label, field_type, is_required, is_active, sort_order, created_at) VALUES ('Globales Pflichtfeld', 'text', 1, 1, 1, ?)")->execute([$now]); $globalField = (int) $pdo->lastInsertId(); $pdo = null; $failures = []; $total = 0; $check = function (bool $ok, string $what) use (&$failures, &$total): void { $total++; if (!$ok) $failures[] = $what; }; $port = random_int(28000, 28999); $log = tempnam(sys_get_temp_dir(), 'dformlog'); $proc = proc_open([PHP_BINARY, '-S', "127.0.0.1:$port", '-t', $tmp], [['pipe', 'r'], ['file', $log, 'w'], ['file', $log, 'a']], $pipes, $tmp); $req = function (string $method, string $path, array $data, string $jar) use ($port): array { $ch = curl_init("http://127.0.0.1:$port$path"); curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_TIMEOUT => 60, CURLOPT_COOKIEJAR => $jar, CURLOPT_COOKIEFILE => $jar]); if ($method === 'POST') { curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data)); } $b = (string) curl_exec($ch); $s = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE); curl_close($ch); return [$s, $b]; }; $csrf = fn(string $h) => preg_match('/name="csrf_token" value="([a-f0-9]+)"/', $h, $m) ? $m[1] : ''; $db = fn() => new PDO('sqlite:' . $GLOBALS['tmp'] . '/database/deskhub.sqlite', null, null, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]); try { for ($i = 0; $i < 50; $i++) { if (@fsockopen('127.0.0.1', $port)) break; usleep(100000); } $jar = tempnam(sys_get_temp_dir(), 'dformjar'); [, $l] = $req('GET', '/staff/login.php', [], $jar); $req('POST', '/staff/login.php', ['csrf_token' => $csrf($l), 'email' => 'admin@test.de', 'password' => 'Sehr-Sicher-1234'], $jar); // 1) Verwaltung [$s, $page] = $req('GET', "/staff/department_form.php?dept={$deptB}", [], $jar); $check($s === 200 && str_contains($page, 'Eigenes Formular für diese Abteilung verwenden'), 'Formular-Seite der Abteilung lädt (HTTP ' . $s . ')'); [, $list] = $req('GET', '/staff/departments.php', [], $jar); $check(str_contains($list, "department_form.php?dept={$deptB}"), 'Abteilungsliste verlinkt das Formular'); $req('POST', "/staff/department_form.php?dept={$deptB}", ['csrf_token' => $csrf($page), 'action' => 'save_form', 'dept' => $deptB, 'own_form' => '1', 'title' => 'Störungsmeldung', 'intro' => 'Bitte genau beschreiben.', 'subject_label' => 'Kurztitel', 'message_label' => 'Was ist passiert?', 'message_hint' => 'Fehlerbild', 'submit_label' => 'Störung melden', 'subject_width' => 'twothirds'], $jar); [, $page] = $req('GET', "/staff/department_form.php?dept={$deptB}", [], $jar); $req('POST', "/staff/department_form.php?dept={$deptB}", ['csrf_token' => $csrf($page), 'action' => 'save_field', 'dept' => $deptB, 'label' => 'Gerätenummer', 'field_type' => 'text', 'is_required' => '1', 'is_active' => '1', 'sort_order' => '10'], $jar); $row = $db()->query("SELECT * FROM custom_fields WHERE department_id = {$deptB}")->fetch(); $check($row && $row['label'] === 'Gerätenummer' && (int) $row['is_required'] === 1, 'Abteilungsfeld wird gespeichert'); $check((int) $db()->query("SELECT own_form FROM departments WHERE id = {$deptB}")->fetchColumn() === 1, 'eigenes Formular ist aktiviert'); $fieldB = (int) ($row['id'] ?? 0); [, $cf] = $req('GET', '/staff/custom_fields.php', [], $jar); $check(!str_contains($cf, 'Gerätenummer') && str_contains($cf, 'Globales Pflichtfeld'), 'Abteilungsfelder erscheinen nicht in der globalen Feldliste'); $db()->exec("INSERT INTO custom_fields (label, field_type, is_required, is_active, sort_order, department_id, form_section, created_at) VALUES ('Telefon', 'text', 0, 1, 20, {$deptB}, 'Ihre Kontaktdaten', '" . date('Y-m-d H:i:s') . "'), ('Kostenstelle', 'text', 0, 1, 30, {$deptB}, 'Abrechnung', '" . date('Y-m-d H:i:s') . "')"); $db()->exec("INSERT INTO custom_fields (label, field_type, is_required, is_active, sort_order, department_id, form_section, created_at) VALUES ('Gerätetyp', 'text', 0, 1, 5, {$deptB}, '@topic', '" . date('Y-m-d H:i:s') . "')"); $db()->exec("INSERT INTO custom_fields (label, field_type, is_required, is_active, sort_order, department_id, form_section, created_at) VALUES ('Titel der Geschichte', 'text', 0, 1, 6, {$deptB}, '@details_top', '" . date('Y-m-d H:i:s') . "'), ('Genre', 'text', 0, 1, 7, {$deptB}, '@details_mid', '" . date('Y-m-d H:i:s') . "')"); $db()->exec("INSERT INTO custom_fields (label, field_type, is_required, is_active, sort_order, department_id, form_section, created_at) VALUES ('GanzOben', 'text', 0, 1, 1, {$deptB}, '@topic_top', '" . date('Y-m-d H:i:s') . "'), ('VorKontakt', 'text', 0, 1, 2, {$deptB}, '@contact_top', '" . date('Y-m-d H:i:s') . "')"); $db()->exec("INSERT INTO custom_fields (label, field_type, options, is_required, is_active, sort_order, department_id, form_section, radio_inline, created_at) VALUES ('Genre-Wahl', 'radio', 'Krimi Liebe', 0, 1, 8, {$deptB}, '@details', 1, '" . date('Y-m-d H:i:s') . "')"); $db()->exec("UPDATE custom_fields SET form_width = 'half' WHERE department_id = {$deptB} AND label IN ('Titel der Geschichte','Genre')"); $db()->exec("INSERT INTO custom_fields (label, field_type, is_required, is_active, sort_order, department_id, form_section, help_text, created_at) VALUES ('Nutzerbedingungen', 'checkbox', 1, 1, 90, {$deptB}, '@details', 'Ich habe die [Nutzungsbedingungen](https://example.org/agb) gelesen. [Böse](javascript:alert(1)) x', '" . date('Y-m-d H:i:s') . "')"); $consentId = (int) $db()->query("SELECT id FROM custom_fields WHERE label = 'Nutzerbedingungen'")->fetchColumn(); // 2) Kundenformular [$s, $form] = $req('GET', '/ticket_new.php', [], tempnam(sys_get_temp_dir(), 'dformc')); $check($s === 200 && str_contains($form, 'Gerätenummer') && str_contains($form, 'Störungsmeldung') && str_contains($form, 'Störung melden'), 'Kundenformular enthält Felder und Texte der Abteilung'); $posI = strpos($form, 'id="issue-title"'); $posC = strpos($form, 'class="contact-title"'); $check($posI !== false && $posC !== false && $posI < $posC, 'Abschnitt „Ihr Anliegen“ steht vor den Kontaktdaten'); $pGo = strpos($form, 'GanzOben'); $pCat = strpos($form, 'id="help_topic_id"'); $pVk = strpos($form, 'VorKontakt'); $pNm = strpos($form, 'id="name"'); $check($pGo !== false && $pGo < $pCat, 'Feld vor der Themenauswahl'); $check($pVk !== false && $pVk < $pNm && $pVk > $posC, 'Feld vor Name/E-Mail im Kontaktbereich'); $check(preg_match('#type="radio" id="custom_\d+_0" name="custom_\d+" value="Krimi"#', $form) === 1 && str_contains($form, 'value="Liebe"'), 'Radio-Auswahl wird als Punkte dargestellt'); $check(substr_count($form, 'custom-field-group fw-half') === 2 && str_contains($form, 'class="dept-fields"'), 'Breite „halbe Zeile“ wird als Klasse ausgegeben, Felder stehen in einer Zeilen-Gruppe'); $check(str_contains($form, '"subject_width":"twothirds"') && str_contains($form, '"message_width":"full"'), 'Breite von Betreff und Beschreibung wird an die Umschaltung übergeben'); $check(str_contains($form, 'display:flex;flex-wrap:wrap;gap:4px 20px'), 'Radio-Optionen können nebeneinander angeordnet werden'); $check(str_contains($form, 'Nutzungsbedingungen'), 'Hinweistext zeigt den Link an'); $check(!str_contains($form, 'href="javascript:') && !str_contains($form, 'x'), 'Unsichere Links und HTML im Hinweistext werden nicht ausgegeben'); $pT = strpos($form, 'Titel der Geschichte'); $pS = strpos($form, 'id="subject-wrap"'); $pG = strpos($form, 'Genre'); $pM = strpos($form, 'id="message-wrap"'); $check($pT !== false && $pT < $pS && $pS < $pG && $pG < $pM, 'Feldposition: vor dem Betreff, zwischen Betreff und Beschreibung'); $posTopicField = strpos($form, 'Gerätetyp'); $check($posTopicField !== false && $posTopicField > $posI && $posTopicField < $posC, 'Feld für Abschnitt 1 (Themenauswahl) steht vor den Kontaktdaten'); $contactBlock = preg_match('#class="contact-title".*?(?=