diff --git a/DeskHub-Webseite/login.php b/DeskHub-Webseite/login.php index 720ac43c..038a5b91 100644 --- a/DeskHub-Webseite/login.php +++ b/DeskHub-Webseite/login.php @@ -132,6 +132,10 @@ $imprintHtml = get_setting('footer_imprint_html', ''); Kein Konto nötig: Ticket erstellen · Status prüfen Mitarbeiter-Login → + + +
+ diff --git a/DeskHub-Webseite/tests/sla_mail_recipients.php b/DeskHub-Webseite/tests/sla_mail_recipients.php new file mode 100644 index 00000000..20d07745 --- /dev/null +++ b/DeskHub-Webseite/tests/sla_mail_recipients.php @@ -0,0 +1,73 @@ +prepare('INSERT INTO departments (name, email) VALUES (?,?)')->execute(['Support', 'support@firma.example']); +$dept = (int) $pdo->lastInsertId(); +$pdo->prepare('INSERT INTO departments (name, email) VALUES (?,?)')->execute(['Technik', 'technik@firma.example']); +$dept2 = (int) $pdo->lastInsertId(); +$person = function (string $name, string $role, ?int $d, string $email = '') use ($pdo, $now): int { + $pdo->prepare('INSERT INTO staff (name, email, password_hash, role, department_id, is_active, created_at) VALUES (?,?,?,?,?,1,?)') + ->execute([$name, $email ?: strtolower($name) . '@firma.example', 'x', $role, $d, $now]); + return (int) $pdo->lastInsertId(); +}; +$anna = $person('Anna', 'agent', $dept); +$ben = $person('Ben', 'agent', $dept); +$cora = $person('Cora', 'agent', $dept2); +$chef = $person('Chef', 'admin', null); +// Admin, dessen Adresse zugleich die Postfach-Adresse ist (wie bei der Fehlermeldung) +$sys = $person('Postfach-Admin', 'admin', null, 'support@firma.example'); +$pdo->prepare('INSERT INTO staff_department (staff_id, department_id, notify) VALUES (?,?,1)')->execute([$cora, $dept]); +set_setting('smtp_from_email', 'noreply@firma.example'); + +// 1) Ticket mit Bearbeiter: nur der Bearbeiter +$r = sla_overdue_recipients($pdo, ['assigned_staff_id' => $anna, 'department_id' => $dept]); +$check($r === ['anna@firma.example'], 'Mit Bearbeiter bekommt nur er die Mail: ' . json_encode($r)); + +// 2) Bearbeiter im Urlaub: seine Vertretung +$pdo->prepare('UPDATE staff SET is_on_vacation = 1, vacation_substitute_id = ? WHERE id = ?')->execute([$ben, $anna]); +$r = sla_overdue_recipients($pdo, ['assigned_staff_id' => $anna, 'department_id' => $dept]); +$check($r === ['ben@firma.example'], 'Ist der Bearbeiter im Urlaub, bekommt die Vertretung die Mail: ' . json_encode($r)); +$pdo->prepare('UPDATE staff SET is_on_vacation = 0, vacation_substitute_id = NULL WHERE id = ?')->execute([$anna]); + +// 3) Ohne Bearbeiter: Mitarbeiter der Abteilung (Haupt- und zusätzliche mit Benachrichtigung), keine Admins, nicht die Abteilungs-Adresse +$r = sla_overdue_recipients($pdo, ['assigned_staff_id' => null, 'department_id' => $dept]); +sort($r); +$check($r === ['anna@firma.example', 'ben@firma.example', 'cora@firma.example'], 'Ohne Bearbeiter: Abteilungsmitarbeiter: ' . json_encode($r)); +$check(!in_array('support@firma.example', $r, true) && !in_array('technik@firma.example', $r, true), 'Die Postfach-Adresse der Abteilung ist nie Empfänger'); +$check(!in_array('chef@firma.example', $r, true), 'Administratoren bekommen nicht automatisch jede SLA-Mail'); + +// 4) Niemand in der Abteilung: Administratoren, aber nie eine Systemadresse +$pdo->exec('UPDATE staff SET is_active = 0 WHERE id IN (' . implode(',', [$anna, $ben, $cora]) . ')'); +$r = sla_overdue_recipients($pdo, ['assigned_staff_id' => null, 'department_id' => $dept]); +$check(in_array('chef@firma.example', $r, true) && !in_array('support@firma.example', $r, true) && !in_array('anna@firma.example', $r, true), 'Ohne Abteilungsmitarbeiter bekommen die Administratoren die Mail, nicht die Postfach-Adresse: ' . json_encode($r)); + +// 5) Schutz vor Mail-Schleifen +$check(mail_is_own_system_mail($pdo, 'kunde@irgendwo.example', "From: x\r\nX-DeskHub-Mail: 1\r\n") === true, 'Die Kopfzeile X-DeskHub-Mail wird erkannt'); +$check(mail_is_own_system_mail($pdo, 'support@firma.example') === true, 'Absender = Abteilungs-Postfach wird erkannt'); +$check(mail_is_own_system_mail($pdo, 'NoReply@Firma.example') === true, 'Absender = Standard-Absender wird erkannt (Groß-/Kleinschreibung egal)'); +$check(mail_is_own_system_mail($pdo, 'kunde@irgendwo.example', "From: kunde@irgendwo.example\r\nSubject: Hilfe\r\n") === false, 'Normale Kundenmails bleiben Tickets'); + +// 6) Die Markierung steckt in den ausgehenden Mails +$src = file_get_contents(dirname(__DIR__) . '/includes/smtp_mailer.php') . file_get_contents(dirname(__DIR__) . '/includes/mailer.php'); +$check(substr_count($src, 'X-DeskHub-Mail: 1') >= 2, 'SMTP- und mail()-Versand setzen die Markierung'); + +@unlink($db); +if ($failures) { + fwrite(STDERR, "SLA-Empfänger / Mail-Schleifen fehlgeschlagen:\n- " . implode("\n- ", $failures) . "\n"); + exit(1); +} +echo "SLA-Empfänger und Schutz vor Mail-Schleifen bestanden.\n"; diff --git a/DeskHub-Webseite/tests/vacation_no_notifications.php b/DeskHub-Webseite/tests/vacation_no_notifications.php new file mode 100644 index 00000000..a7bf6d1b --- /dev/null +++ b/DeskHub-Webseite/tests/vacation_no_notifications.php @@ -0,0 +1,134 @@ +prepare('INSERT INTO departments (name, email) VALUES (?,?)')->execute(['Support', 'support@firma.example']); +$dept = (int) $pdo->lastInsertId(); +$person = function (string $name, string $role, int $d) use ($pdo, $now): int { + $pdo->prepare('INSERT INTO staff (name, email, password_hash, role, department_id, is_active, created_at) VALUES (?,?,?,?,?,1,?)') + ->execute([$name, strtolower($name) . '@firma.example', 'x', $role, $d, $now]); + return (int) $pdo->lastInsertId(); +}; +$anna = $person('Anna', 'agent', $dept); +$ben = $person('Ben', 'agent', $dept); +$chef = $person('Chef', 'admin', $dept); +$away = fn(int $id, bool $on, ?int $sub = null) => $pdo->prepare('UPDATE staff SET is_on_vacation = ?, vacation_substitute_id = ? WHERE id = ?')->execute([$on ? 1 : 0, $sub, $id]); + +// Mails über die Warteschlange abfangen: SMTP absichtlich ohne Server, Mails landen in mail_queue +set_setting('mail_enabled', '1'); +set_setting('smtp_host', '127.0.0.1'); +set_setting('smtp_port', '1'); +set_setting('smtp_encryption', 'none'); +set_setting('smtp_from_email', 'noreply@firma.example'); +$queued = function () use ($pdo): array { return $pdo->query('SELECT to_email FROM mail_queue ORDER BY id')->fetchAll(PDO::FETCH_COLUMN); }; +$reset = fn() => $pdo->exec('DELETE FROM mail_queue'); +$ticket = ['id' => 1, 'ticket_number' => '261030-0001', 'subject' => 'Test', 'requester_name' => 'Kunde', 'requester_email' => 'kunde@x.example', 'department_id' => $dept, 'department_name' => 'Support', 'assigned_staff_id' => null]; + +// 1) Neues Ticket +$reset(); +notify_staff_new_ticket($ticket); +$to = $queued(); sort($to); +$check(in_array('anna@firma.example', $to, true) && in_array('ben@firma.example', $to, true) && in_array('chef@firma.example', $to, true), 'Ohne Urlaub gehen Mails an alle: ' . json_encode($to)); +$reset(); +$away($ben, true); +notify_staff_new_ticket($ticket); +$check(!in_array('ben@firma.example', $queued(), true), 'Neues Ticket: Mitarbeiter im Urlaub bekommt keine Mail'); +$away($ben, false); + +// 2) Überfällig an Admins +$reset(); +$away($chef, true); +notify_admins_overdue_ticket($ticket); +$check(!in_array('chef@firma.example', $queued(), true), 'Überfällig: Administrator im Urlaub bekommt keine Mail'); +$away($chef, false); + +// 3) SLA-Empfänger: Bearbeiter im Urlaub ohne Vertretung bekommt nichts, mit Vertretung bekommt diese die Mail +$away($anna, true); +$r = sla_overdue_recipients($pdo, ['assigned_staff_id' => $anna, 'department_id' => $dept]); +$check(!in_array('anna@firma.example', $r, true), 'SLA: Bearbeiter im Urlaub ohne Vertretung bekommt keine Mail: ' . json_encode($r)); +$away($anna, true, $ben); +$r = sla_overdue_recipients($pdo, ['assigned_staff_id' => $anna, 'department_id' => $dept]); +$check($r === ['ben@firma.example'], 'SLA: mit Vertretung bekommt diese die Mail: ' . json_encode($r)); +$away($anna, false); + +// 4) Erwähnung per Mail +$pdo->prepare("INSERT INTO tickets (ticket_number, subject, status, department_id, requester_name, requester_email, priority_id, created_at, updated_at) VALUES (?,?,?,?,?,?,(SELECT MIN(id) FROM priorities),?,?)") + ->execute(['261030-0001', 'Test', 'open', $dept, 'Kunde', 'kunde@x.example', $now, $now]); +$ticket['id'] = (int) $pdo->lastInsertId(); +$pdo->prepare("INSERT INTO ticket_threads (ticket_id, author_type, message, is_internal_note, created_at) VALUES (?,?,?,0,?)")->execute([$ticket['id'], 'staff', 'x', $now]); +$threadId = (int) $pdo->lastInsertId(); +$reset(); +$away($ben, true); +if (true) { + save_mentions($pdo, (int) $ticket['id'], $threadId, $anna, '@Ben bitte prüfen', $ticket + ['ticket_number' => '261030-0001']); + $check(!in_array('ben@firma.example', $queued(), true), 'Erwähnung: Mitarbeiter im Urlaub bekommt keine Mail'); +} +$away($ben, false); + +// 5) Push-Hilfsfunktion und Abfragen +$check(staff_is_on_vacation($ben) === false, 'Ben ist zurück'); +$away($ben, true); +$check(staff_is_on_vacation($ben) === true, 'Ben ist im Urlaub'); +$src = file_get_contents(dirname(__DIR__) . '/includes/push.php') . file_get_contents(dirname(__DIR__) . '/includes/fcm.php'); +$check(str_contains($src, 'vacation_expand_ids') && substr_count($src, 'vacation_expand_ids') >= 3, 'Web-Push und Android-Push ersetzen Mitarbeiter im Urlaub durch die Vertretung'); +$away($ben, false); + +// 6) Android-Push: Geräte von Mitarbeitern im Urlaub werden übersprungen +$pdo->prepare('INSERT INTO push_devices (staff_id, token, token_hash, created_at) VALUES (?,?,?,?)')->execute([$anna, str_repeat('a', 40), hash('sha256', 'a'), $now]); +$pdo->prepare('INSERT INTO push_devices (staff_id, token, token_hash, created_at) VALUES (?,?,?,?)')->execute([$ben, str_repeat('b', 40), hash('sha256', 'b'), $now]); +$away($ben, true); +$sel = $pdo->prepare("SELECT d.staff_id FROM push_devices d JOIN staff s ON s.id = d.staff_id WHERE s.is_active = 1 AND s.is_on_vacation = 0 AND d.staff_id IN (?,?)"); +$sel->execute([$anna, $ben]); +$check($sel->fetchAll(PDO::FETCH_COLUMN) == [$anna], 'Android: nur das Gerät des anwesenden Mitarbeiters bleibt'); + +// 7) Die Vertretung bekommt stattdessen die Benachrichtigung +$reset(); +$away($ben, true, $anna); +notify_staff_new_ticket($ticket); +$to = $queued(); +$check(in_array('anna@firma.example', $to, true) && !in_array('ben@firma.example', $to, true), 'Neues Ticket: die Vertretung bekommt die Mail des Abwesenden: ' . json_encode($to)); +$check(count(array_keys($to, 'anna@firma.example', true)) === 1, 'Die Vertretung bekommt die Mail nur einmal, auch wenn sie selbst Empfänger ist'); +$reset(); +$away($chef, true, $ben); +$away($ben, true, $anna); +notify_admins_overdue_ticket($ticket); +$to = $queued(); +$check(in_array('anna@firma.example', $to, true) && !in_array('chef@firma.example', $to, true) && !in_array('ben@firma.example', $to, true), 'Kette: Admin im Urlaub → Vertretung im Urlaub → deren Vertretung bekommt die Mail: ' . json_encode($to)); +$away($chef, false); +$away($ben, false); +// Push: Abos der Abwesenden gehen an die Vertretung +$sub = fn(int $id, string $ep) => $pdo->prepare('INSERT INTO push_subscriptions (staff_id, endpoint, p256dh, auth, created_at) VALUES (?,?,?,?,?)')->execute([$id, $ep, 'k', 'a', $now]); +$sub($anna, 'https://push.example/anna'); +$sub($ben, 'https://push.example/ben'); +$endpoints = fn() => array_map(fn($r) => $r['endpoint'], push_subscriptions_for_department($pdo, $dept)); +$check(in_array('https://push.example/ben', $endpoints(), true), 'Ohne Urlaub bekommt Ben das Browser-Push'); +$away($ben, true, $anna); +$e = $endpoints(); +$check(!in_array('https://push.example/ben', $e, true) && in_array('https://push.example/anna', $e, true), 'Im Urlaub geht das Browser-Push an die Vertretung: ' . json_encode($e)); +$away($ben, true, null); +$e = $endpoints(); +$check(!in_array('https://push.example/ben', $e, true), 'Ohne Vertretung bekommt der Abwesende nichts'); +$check(vacation_expand_ids($pdo, [$ben]) === [], 'Ohne Vertretung entfällt der Abwesende'); +$away($ben, true, $anna); +$check(vacation_expand_ids($pdo, [$ben, $anna]) === [$anna], 'Vertretung wird nicht doppelt gezählt'); +$away($ben, false); +@unlink($db); +if ($failures) { + fwrite(STDERR, "Urlaub ohne Benachrichtigungen fehlgeschlagen:\n- " . implode("\n- ", $failures) . "\n"); + exit(1); +} +echo "Urlaub ohne Benachrichtigungen bestanden.\n"; diff --git a/DeskHub-Webseite/tests/vacation_substitute_access.php b/DeskHub-Webseite/tests/vacation_substitute_access.php new file mode 100644 index 00000000..1ce33ca0 --- /dev/null +++ b/DeskHub-Webseite/tests/vacation_substitute_access.php @@ -0,0 +1,145 @@ +prepare('INSERT INTO departments (name, email) VALUES (?,?)')->execute([$name, strtolower($name) . '@firma.example']); + return (int) $pdo->lastInsertId(); +}; +$person = function (string $name, string $role, ?int $dept) use ($pdo, $now): int { + $pdo->prepare('INSERT INTO staff (name, email, password_hash, role, department_id, is_active, created_at) VALUES (?,?,?,?,?,1,?)') + ->execute([$name, strtolower($name) . '@firma.example', 'x', $role, $dept, $now]); + return (int) $pdo->lastInsertId(); +}; +$staffRow = fn(int $id) => $pdo->query('SELECT * FROM staff WHERE id = ' . $id)->fetch(); +$ids = function (int $id) use ($staffRow): ?array { $r = staff_department_ids($staffRow($id)); if ($r !== null) sort($r); return $r; }; +$away = fn(int $id, ?int $sub) => $pdo->prepare('UPDATE staff SET is_on_vacation = ?, vacation_substitute_id = ? WHERE id = ?')->execute([$sub === null ? 0 : 1, $sub, $id]); + +$dA = $dept('Support'); $dB = $dept('Technik'); $dC = $dept('Buchhaltung'); +$anna = $person('Anna', 'agent', $dA); // Vertretung, nur Support +$ben = $person('Ben', 'agent', $dB); // im Urlaub, Technik +$cora = $person('Cora', 'agent', $dC); // Kette +$chef = $person('Chef', 'admin', null); +$pdo->prepare('INSERT INTO staff_department (staff_id, department_id) VALUES (?,?)')->execute([$ben, $dC]); // Ben hat zusätzlich Buchhaltung + +// 1) Ohne Abwesenheit: nur die eigene Abteilung +$check($ids($anna) === [$dA], 'Ohne Abwesenheit hat Anna nur ihre Abteilung'); +$check($ids($ben) === [$dB, $dC], 'Ben hat Technik und Buchhaltung'); + +// 2) Ben im Urlaub, Anna ist die Vertretung +$away($ben, $anna); +$check($ids($anna) === [$dA, $dB, $dC], 'Die Vertretung bekommt Haupt- und zusätzliche Abteilungen des Abwesenden'); +$check($ids($ben) === [$dB, $dC], 'Der Abwesende selbst behält seinen Zugriff'); +$check($ids($cora) === [$dC], 'Unbeteiligte bekommen nichts dazu'); +[$sql, $params] = dept_filter_sql($ids($anna), 't.department_id'); +$check(str_contains($sql, 'IN') && count($params) === 3, 'Die Ticketliste filtert mit den erweiterten Abteilungen'); +$check(staff_can_access_department($staffRow($anna), $dB) === true, 'Die Vertretung darf Tickets der Abteilung öffnen'); + +// 3) Kette: Anna ist selbst im Urlaub, Cora vertritt Anna +$away($anna, $cora); +$check($ids($cora) === [$dA, $dB, $dC], 'Kette: die Vertretung der Vertretung bekommt beide Abteilungen'); +$away($anna, null); + +// 4) Ende der Abwesenheit +$away($ben, null); +$check($ids($anna) === [$dA], 'Nach dem Urlaub ist der zusätzliche Zugriff wieder weg'); + +// 5) Inaktiver Abwesender zählt nicht +$away($ben, $anna); +$pdo->exec('UPDATE staff SET is_active = 0 WHERE id = ' . $ben); +$check($ids($anna) === [$dA], 'Ein inaktiver Abwesender gibt keinen Zugriff'); +$pdo->exec('UPDATE staff SET is_active = 1 WHERE id = ' . $ben); +$away($ben, null); + +// 6) Abwesender Administrator: nur Abteilungen der zugewiesenen offenen Tickets +$away($chef, $anna); +$check($ids($anna) === [$dA], 'Abwesender Admin ohne Tickets: kein zusätzlicher Zugriff'); +$pdo->prepare("INSERT INTO tickets (ticket_number, subject, status, department_id, assigned_staff_id, requester_name, requester_email, priority_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?,(SELECT MIN(id) FROM priorities),?,?)") + ->execute(['T-1', 'Test', 'open', $dB, $anna, 'Kunde', 'kunde@x.example', $now, $now]); +$check($ids($anna) === [$dA, $dB], 'Abwesender Admin: die Abteilung des zugewiesenen Tickets kommt dazu'); +$pdo->exec("UPDATE tickets SET status = 'closed'"); +$check($ids($anna) === [$dA], 'Geschlossene Tickets geben keinen Zugriff mehr'); +$away($chef, null); + +// 7) Admin bleibt unbeschränkt, Selbstvertretung wird ignoriert +$check($ids($chef) === null, 'Administratoren sehen weiter alle Abteilungen'); +$away($anna, $anna); +$check($ids($anna) === [$dA], 'Selbst als Vertretung eingetragen: kein Zugriffsgewinn'); +$away($anna, null); + +// 8) Weitergabe und Rückgabe der Tickets (Profil-Abwesenheit) +$newTicket = function (string $nr, int $deptId, ?int $assignee, string $status = 'open') use ($pdo, $now): int { + $pdo->prepare("INSERT INTO tickets (ticket_number, subject, status, department_id, assigned_staff_id, requester_name, requester_email, priority_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?,(SELECT MIN(id) FROM priorities),?,?)") + ->execute([$nr, 'Test ' . $nr, $status, $deptId, $assignee, 'Kunde', 'kunde@x.example', $now, $now]); + return (int) $pdo->lastInsertId(); +}; +$owner = fn(int $t) => (int) $pdo->query('SELECT assigned_staff_id FROM tickets WHERE id = ' . $t)->fetchColumn(); +$t1 = $newTicket('V-1', $dB, $ben); +$t2 = $newTicket('V-2', $dB, $ben); +$t3 = $newTicket('V-3', $dB, $ben, 'closed'); +$t4 = $newTicket('V-4', $dB, $chef); +$pdo->prepare('UPDATE staff SET is_on_vacation = 1, vacation_substitute_id = ? WHERE id = ?')->execute([$anna, $ben]); +[$h, $b] = vacation_apply_change($pdo, $ben, [0, null], [1, $anna], false); +$check($h === 0 && $owner($t1) === $ben, 'Ohne Haken wird nichts weitergegeben'); +[$h, $b] = vacation_apply_change($pdo, $ben, [0, null], [1, $anna], true); +$check($h === 2 && $owner($t1) === $anna && $owner($t2) === $anna, 'Offene Tickets gehen an die Vertretung'); +$check($owner($t3) === $ben && $owner($t4) === $chef, 'Geschlossene und fremde Tickets bleiben'); +$check((int) $pdo->query('SELECT COUNT(*) FROM vacation_moves WHERE returned_at IS NULL')->fetchColumn() === 2, 'Die Weitergabe wird gemerkt'); +$check((int) $pdo->query("SELECT COUNT(*) FROM activity_log WHERE action LIKE 'An die Vertretung%'")->fetchColumn() === 2, 'Das Ticket-Protokoll nennt die Weitergabe'); +[$h] = vacation_apply_change($pdo, $ben, [1, $anna], [1, $anna], true); +$check($h === 0, 'Erneutes Speichern gibt nichts doppelt weiter'); +// Anna ändert eines der Tickets selbst auf erledigt, ein anderes hängt jemand um +$pdo->exec("UPDATE tickets SET status = 'resolved' WHERE id = " . $t1); +$pdo->prepare('UPDATE tickets SET assigned_staff_id = ? WHERE id = ?')->execute([$cora, $t2]); +// Ende des Urlaubs +$pdo->prepare('UPDATE staff SET is_on_vacation = 0, vacation_substitute_id = NULL WHERE id = ?')->execute([$ben]); +[$h, $b] = vacation_apply_change($pdo, $ben, [1, $anna], [0, null], true); +$check($b === 0 && $owner($t1) === $anna && $owner($t2) === $cora, 'Erledigte und umgehängte Tickets kommen nicht zurück'); +$check((int) $pdo->query('SELECT COUNT(*) FROM vacation_moves WHERE returned_at IS NULL')->fetchColumn() === 0, 'Alle Weitergaben sind abgeschlossen'); +// Zurückgeben, wenn alles offen bei der Vertretung liegt +$t5 = $newTicket('V-5', $dB, $ben); +$pdo->prepare('UPDATE staff SET is_on_vacation = 1, vacation_substitute_id = ? WHERE id = ?')->execute([$anna, $ben]); +vacation_apply_change($pdo, $ben, [0, null], [1, $anna], true); +$check($owner($t5) === $anna, 'Weitergabe (zweiter Urlaub)'); +$pdo->prepare('UPDATE staff SET is_on_vacation = 0, vacation_substitute_id = NULL WHERE id = ?')->execute([$ben]); +[$h, $b] = vacation_apply_change($pdo, $ben, [1, $anna], [0, null], true); +$check($b === 1 && $owner($t5) === $ben, 'Offene Tickets kommen nach dem Urlaub zurück'); +// Neue Vertretung: erst zurück, dann an die neue +$t6 = $newTicket('V-6', $dB, $ben); +$pdo->prepare('UPDATE staff SET is_on_vacation = 1, vacation_substitute_id = ? WHERE id = ?')->execute([$anna, $ben]); +vacation_apply_change($pdo, $ben, [0, null], [1, $anna], true); +$pdo->prepare('UPDATE staff SET vacation_substitute_id = ? WHERE id = ?')->execute([$cora, $ben]); +vacation_apply_change($pdo, $ben, [1, $anna], [1, $cora], true); +$check($owner($t6) === $cora, 'Bei neuer Vertretung geht das Ticket an die neue Person'); +// Ohne Vertretung / Vertretung selbst im Urlaub ohne Ersatz: nichts +$pdo->prepare('UPDATE staff SET is_on_vacation = 0, vacation_substitute_id = NULL WHERE id = ?')->execute([$ben]); +vacation_apply_change($pdo, $ben, [1, $cora], [0, null], true); +$check($owner($t6) === $ben, 'Zurück bei Ben'); +$pdo->prepare('UPDATE staff SET is_on_vacation = 1, vacation_substitute_id = NULL WHERE id = ?')->execute([$ben]); +[$h] = vacation_apply_change($pdo, $ben, [0, null], [1, null], true); +$check($h === 0 && $owner($t6) === $ben, 'Ohne Vertretung bleibt alles beim Abwesenden'); +// Das Plugin „Urlaub und Vertretung“ hat Vorrang +$pdo->exec('CREATE TABLE IF NOT EXISTS plugin_vertretung_absences (id INTEGER PRIMARY KEY AUTOINCREMENT, staff_id INTEGER, substitute_id INTEGER, start_date TEXT, end_date TEXT, note TEXT, status TEXT, created_by INTEGER, created_at TEXT)'); +$pdo->prepare("INSERT INTO plugin_vertretung_absences (staff_id, substitute_id, start_date, end_date, note, status, created_at) VALUES (?,?,?,?,?,?,?)")->execute([$ben, $anna, '2026-01-01', '2026-12-31', '', 'active', $now]); +$pdo->prepare('UPDATE staff SET is_on_vacation = 1, vacation_substitute_id = ? WHERE id = ?')->execute([$anna, $ben]); +[$h] = vacation_apply_change($pdo, $ben, [0, null], [1, $anna], true); +$check($h === 0 && $owner($t6) === $ben, 'Ist eine Abwesenheit im Plugin aktiv, gibt der Server nichts selbst weiter'); +@unlink($db); +if ($failures) { + fwrite(STDERR, "Vertretungs-Zugriff fehlgeschlagen:\n- " . implode("\n- ", $failures) . "\n"); + exit(1); +} +echo "Vertretungs-Zugriff bestanden.\n"; diff --git a/DeskHub-Webseite/tests/web_smoke_integration.php b/DeskHub-Webseite/tests/web_smoke_integration.php index bf84b72f..f170eb6a 100644 --- a/DeskHub-Webseite/tests/web_smoke_integration.php +++ b/DeskHub-Webseite/tests/web_smoke_integration.php @@ -150,7 +150,7 @@ try { '/staff/custom_lists.php' => 'Listen', '/staff/custom_lists.php?new=1' => 'Einträge', '/staff/site_pages.php' => 'Seiten', '/staff/site_pages.php?new=1' => 'Seiteninformationen', '/staff/business_hours.php' => 'Feiertage', '/staff/business_hours.php?tab=holidays' => 'Feiertage', - '/staff/update.php' => 'Datenbank aktualisieren', '/staff/plugins.php' => 'Plugin-Katalog', '/staff/backup.php' => 'Vorhandene Backups', '/staff/security_check.php' => 'Sicherheitsprüfung', '/staff/import_osticket.php' => 'osTicket Import', '/staff/ticket_rules.php' => 'Ticket-Regeln', '/staff/ticket_rules.php?new=1' => 'Regelinformationen', + '/staff/update.php' => 'Datenbank aktualisieren', '/staff/plugins.php' => 'Plugin-Katalog', '/staff/login_log.php' => 'Anmelde-Wächter', '/staff/login_alert.php?t=abc' => 'ungültig, abgelaufen', '/staff/backup.php' => 'Vorhandene Backups', '/staff/security_check.php' => 'Sicherheitsprüfung', '/staff/import_osticket.php' => 'osTicket Import', '/staff/ticket_rules.php' => 'Ticket-Regeln', '/staff/ticket_rules.php?new=1' => 'Regelinformationen', ] as $path => $marker) { $response = browser_request($port, 'GET', $path, $cookies); assert_page($response, $marker); diff --git a/DeskHub-Webseite/tools/lang_pack_add.php b/DeskHub-Webseite/tools/lang_pack_add.php new file mode 100644 index 00000000..91ff4d73 --- /dev/null +++ b/DeskHub-Webseite/tools/lang_pack_add.php @@ -0,0 +1,43 @@ + + * neue-texte.json: {"Deutscher Text": "Übersetzung", …} (wie strings.json). Vorhandene Schlüssel werden überschrieben. + */ +if ($argc < 4) { fwrite(STDERR, "Aufruf: php tools/lang_pack_add.php \n"); exit(1); } +[$_, $old, $newJson, $out] = $argv; +$add = json_decode((string) file_get_contents($newJson), true); +if (!is_array($add)) { fwrite(STDERR, "Die Datei mit den neuen Texten ist kein gültiges JSON.\n"); exit(1); } +$zip = new ZipArchive(); +if ($zip->open($old) !== true) { fwrite(STDERR, "Das alte ZIP lässt sich nicht öffnen.\n"); exit(1); } +$files = []; +for ($i = 0; $i < $zip->numFiles; $i++) $files[str_replace(chr(92), '/', $zip->getNameIndex($i))] = $zip->getFromIndex($i); +$zip->close(); +$strKey = $manKey = null; +foreach ($files as $name => $_c) { + if (str_ends_with($name, '/strings.json')) $strKey = $name; + if (str_ends_with($name, '/plugin.json')) $manKey = $name; +} +if (!$strKey || !$manKey) { fwrite(STDERR, "strings.json oder plugin.json fehlt im ZIP.\n"); exit(1); } +$dict = json_decode($files[$strKey], true); +$info = ['_info' => $dict['_info'] ?? '']; +unset($dict['_info']); +foreach ($add as $k => $v) { + if (!is_string($k) || !is_string($v) || $k === '' || $k !== trim($k) && false) continue; + if (substr_count($k, '{') !== substr_count($v, '{')) { fwrite(STDERR, "Platzhalter ungleich: $k\n"); continue; } + $dict[$k] = $v; +} +ksort($dict, SORT_STRING); +$files[$strKey] = json_encode($info + $dict, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . "\n"; +if (!preg_match('/"version"\s*:\s*"(\d+)\.(\d+)\.(\d+)"/', $files[$manKey], $m)) { fwrite(STDERR, "Version nicht gefunden.\n"); exit(1); } +$ver = $m[1] . '.' . $m[2] . '.' . ((int) $m[3] + 1); +$files[$manKey] = preg_replace('/("version"\s*:\s*")\d+\.\d+\.\d+(")/', '${1}' . $ver . '${2}', $files[$manKey], 1); +@unlink($out); +$z = new ZipArchive(); +$z->open($out, ZipArchive::CREATE); +ksort($files); +foreach ($files as $name => $content) $z->addFromString($name, $content); +$z->close(); +echo basename($out), ' Version ', $ver, ' (', count($dict), ' Texte) ', hash_file('sha256', $out), "\n";