Upload via GUI (360 Dateien)
This commit is contained in:
@@ -132,6 +132,10 @@ $imprintHtml = get_setting('footer_imprint_html', '');
|
||||
<span class="login-guest-links">Kein Konto nötig: <a href="/ticket_new.php">Ticket erstellen</a> · <a href="/ticket_lookup.php">Status prüfen</a></span>
|
||||
<a href="/staff/login.php" class="login-staff-link">Mitarbeiter-Login →</a>
|
||||
</div>
|
||||
<?php $loginButtons = (string) plugin_filter('customer.login.buttons', '', []); ?>
|
||||
<?php if ($loginButtons !== ''): ?>
|
||||
<div class="login-alt" style="margin-top:18px;padding-top:16px;border-top:1px solid #e2e8f0;display:grid;gap:10px"><?= $loginButtons ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Prüft, wer bei einer SLA-Überschreitung eine Mail bekommt (nur Betroffene, nie die Postfach-Adresse der Abteilung) und den Schutz vor
|
||||
* Mail-Schleifen (eigene Systemmails werden vom E-Mail-Abruf nicht zu Tickets).
|
||||
*/
|
||||
|
||||
$db = sys_get_temp_dir() . '/deskhub-sla-' . bin2hex(random_bytes(4)) . '.sqlite';
|
||||
putenv('DESKHUB_TESTING=1');
|
||||
putenv('DESKHUB_SQLITE_PATH=' . $db);
|
||||
require_once dirname(__DIR__) . '/includes/bootstrap.php';
|
||||
|
||||
$failures = [];
|
||||
$check = function (bool $ok, string $what) use (&$failures): void { if (!$ok) $failures[] = $what; };
|
||||
$pdo = get_db();
|
||||
$now = db_now();
|
||||
$pdo->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";
|
||||
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Prüft, dass Mitarbeiter im Urlaub keine Benachrichtigungen bekommen: E-Mail (neues Ticket, überfällig, Erwähnung, Team, Eskalation),
|
||||
* Browser-Push und Android-Push. Mitarbeiter, die zurück sind, bekommen sie wieder.
|
||||
*/
|
||||
|
||||
$db = sys_get_temp_dir() . '/deskhub-vn-' . bin2hex(random_bytes(4)) . '.sqlite';
|
||||
putenv('DESKHUB_TESTING=1');
|
||||
putenv('DESKHUB_SQLITE_PATH=' . $db);
|
||||
require_once dirname(__DIR__) . '/includes/bootstrap.php';
|
||||
require_once dirname(__DIR__) . '/includes/push.php';
|
||||
|
||||
$failures = [];
|
||||
$check = function (bool $ok, string $what) use (&$failures): void { if (!$ok) $failures[] = $what; };
|
||||
$pdo = get_db();
|
||||
$now = db_now();
|
||||
$pdo->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";
|
||||
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Prüft den Zugriff der Vertretung: Wer einen Abwesenden vertritt, sieht und bearbeitet dessen Abteilungen mit, solange die Abwesenheit
|
||||
* dauert, auch ohne eigenen Zugang zur Abteilung. Ketten, abwesende Administratoren, Ende der Abwesenheit, inaktive Mitarbeiter.
|
||||
*/
|
||||
|
||||
$db = sys_get_temp_dir() . '/deskhub-vs-' . bin2hex(random_bytes(4)) . '.sqlite';
|
||||
putenv('DESKHUB_TESTING=1');
|
||||
putenv('DESKHUB_SQLITE_PATH=' . $db);
|
||||
require_once dirname(__DIR__) . '/includes/bootstrap.php';
|
||||
|
||||
$failures = [];
|
||||
$check = function (bool $ok, string $what) use (&$failures): void { if (!$ok) $failures[] = $what; };
|
||||
$pdo = get_db();
|
||||
$now = db_now();
|
||||
$dept = function (string $name) use ($pdo): int {
|
||||
$pdo->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";
|
||||
@@ -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);
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
/**
|
||||
* tools/lang_pack_add.php
|
||||
*
|
||||
* Ergänzt ein Sprachpaket-ZIP (zum Beispiel lang-en) um Übersetzungen, erhöht die Version und schreibt ein neues ZIP.
|
||||
* php tools/lang_pack_add.php <altes.zip> <neue-texte.json> <neues.zip>
|
||||
* 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 <altes.zip> <neue-texte.json> <neues.zip>\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";
|
||||
Reference in New Issue
Block a user