0) ob_end_clean(); header('Content-Type: application/json'); echo json_encode(['ok' => false, 'error' => $err['message'] . ' in ' . basename($err['file']) . ':' . $err['line']]); } }); } @ini_set('memory_limit', '1024M'); @set_time_limit(300); function import_json(array $data): never { while (ob_get_level() > 0) ob_end_clean(); header('Content-Type: application/json'); echo json_encode($data, JSON_INVALID_UTF8_SUBSTITUTE); exit; } function import_ajax_guard(): void { header('Content-Type: application/json'); if (!hash_equals($_SESSION['csrf_token'] ?? '', (string) ($_POST['csrf_token'] ?? ''))) { // Ist eine Anfrage größer als post_max_size, verwirft PHP alle Formulardaten: dann fehlt auch das Token import_json(['ok' => false, 'error' => 'Die Sitzung ist abgelaufen oder der Upload-Teil war größer als der Server erlaubt (post_max_size ' . ini_get('post_max_size') . '). Bitte die Seite neu laden und erneut versuchen.']); } } $stateDir = osticket_state_dir(); /** PHP-Größenangabe ("2M", "128K", "1G") in Bytes. */ function import_ini_bytes(string $v): int { $v = trim($v); if ($v === '' || $v === '-1') return PHP_INT_MAX; $n = (float) $v; return (int) match (strtolower(substr($v, -1))) { 'g' => $n * 1073741824, 'm' => $n * 1048576, 'k' => $n * 1024, default => $n }; } /** Größe eines Upload-Teils: so groß wie der Server erlaubt (höchstens 5 MB, mit Reserve), damit der Upload auch bei kleinen Limits (z. B. 2 MB) klappt. */ function import_chunk_size(): int { $limit = min(import_ini_bytes((string) ini_get('upload_max_filesize')), import_ini_bytes((string) ini_get('post_max_size'))); $size = (int) ($limit * 0.8); $size = intdiv($size, 65536) * 65536; return max(131072, min(5 * 1048576, $size)); } /** Verständliche Meldung zu einem PHP-Upload-Fehlercode. */ function import_upload_error(?array $file): string { $code = $file['error'] ?? UPLOAD_ERR_NO_FILE; $limit = ini_get('upload_max_filesize') . ' / post_max_size ' . ini_get('post_max_size'); return match ($code) { UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE => 'Der Server lehnt den Upload-Teil ab, er ist größer als erlaubt (upload_max_filesize ' . $limit . '). Laden Sie die Seite neu und versuchen Sie es erneut, oder legen Sie die Datei per FTP in den DeskHub-Ordner (Option 1).', UPLOAD_ERR_PARTIAL => 'Der Upload-Teil kam nur unvollständig an (Verbindung unterbrochen). Bitte erneut versuchen.', UPLOAD_ERR_NO_FILE => 'Es kam kein Upload-Teil an.', UPLOAD_ERR_NO_TMP_DIR => 'Auf dem Server fehlt der temporäre Ordner für Uploads (upload_tmp_dir). Bitte den Hoster fragen oder die Datei per FTP hochladen (Option 1).', UPLOAD_ERR_CANT_WRITE => 'Der Server konnte den Upload-Teil nicht speichern (Speicherplatz oder Schreibrechte). Bitte die Datei per FTP hochladen (Option 1).', default => 'Upload fehlgeschlagen (PHP-Fehlercode ' . $code . ').', }; } // ── Chunked Upload ───────────────────────────────────────────────────────── if (($_POST['action'] ?? '') === 'upload_chunk') { import_ajax_guard(); $chunk = $_FILES['chunk'] ?? null; $idx = (int) ($_POST['chunk_index'] ?? 0); $total = (int) ($_POST['total_chunks'] ?? 1); $tmpFile = $stateDir . '/upload.sql'; if (!$chunk || $chunk['error'] !== UPLOAD_ERR_OK) import_json(['ok' => false, 'error' => import_upload_error($chunk)]); $fh = @fopen($tmpFile, $idx === 0 ? 'wb' : 'ab'); if (!$fh) import_json(['ok' => false, 'error' => 'Die Datei konnte nicht gespeichert werden: Der Import-Ordner ist nicht beschreibbar (' . $stateDir . ').']); $written = fwrite($fh, (string) file_get_contents($chunk['tmp_name'])); fclose($fh); if ($written === false || $written < (int) filesize($chunk['tmp_name'])) import_json(['ok' => false, 'error' => 'Die Datei konnte nicht vollständig gespeichert werden (Speicherplatz voll?).']); $done = ($idx + 1) >= $total; if ($done) { $_SESSION['import_sql_path'] = $tmpFile; $_SESSION['import_filename'] = basename((string) ($_POST['filename'] ?? 'import.sql')); @unlink($stateDir . '/state.json'); @unlink($stateDir . '/entries.jsonl'); @unlink($stateDir . '/entry_map.jsonl'); } import_json(['ok' => true, 'done' => $done, 'chunk' => $idx]); } // ── Dateipfad ────────────────────────────────────────────────────────────── if (($_POST['action'] ?? '') === 'use_path') { verify_csrf(); $filename = basename(trim($_POST['sql_filename'] ?? '')); $fullPath = realpath(__DIR__ . '/../' . $filename); $rootDir = realpath(__DIR__ . '/..'); if ($filename === '') { flash('error', 'Bitte den Dateinamen der SQL-Datei eintragen (das graue Beispiel im Feld ist nur ein Platzhalter).'); redirect('/staff/import_osticket.php'); } if (!$fullPath || strpos($fullPath, $rootDir) !== 0 || !is_file($fullPath)) { flash('error', 'Datei nicht gefunden: ' . $filename . '. Sie muss im DeskHub-Hauptordner auf dem Server liegen (dort, wo auch index.php liegt).'); redirect('/staff/import_osticket.php'); } $_SESSION['import_sql_path'] = $fullPath; $_SESSION['import_filename'] = $filename; foreach (['state.json', 'entries.jsonl', 'entry_map.jsonl'] as $f) @unlink($stateDir . '/' . $f); redirect('/staff/import_osticket.php?step=analyze'); } // ── Analyse ──────────────────────────────────────────────────────────────── if (($_POST['action'] ?? '') === 'analyze_chunk') { import_ajax_guard(); $tmp = $_SESSION['import_sql_path'] ?? ''; if (!$tmp || !file_exists($tmp)) import_json(['ok' => false, 'error' => 'Datei fehlt']); try { $res = osticket_analyze_step($tmp, $stateDir, (int) ($_POST['byte_offset'] ?? 0)); } catch (Throwable $e) { import_json(['ok' => false, 'error' => $e->getMessage()]); } import_json(['ok' => true, 'done' => $res['done'], 'byte_offset' => $res['offset'], 'file_size' => filesize($tmp), 'counts' => $res['counts']]); } // ── Import starten ───────────────────────────────────────────────────────── if (($_POST['action'] ?? '') === 'start') { verify_csrf(); $allowed = ['users', 'structure', 'tickets', 'threads', 'attachments']; $_SESSION['import_what'] = array_values(array_intersect($allowed, (array) ($_POST['import'] ?? []))); redirect('/staff/import_osticket.php?step=run'); } // ── Reset ────────────────────────────────────────────────────────────────── if (($_POST['action'] ?? '') === 'reset_import') { import_ajax_guard(); $type = $_POST['reset_type'] ?? 'all'; if ($type === 'threads_only') { $pdo->exec('DELETE FROM ticket_attachments'); $pdo->exec('DELETE FROM ticket_threads'); } else { $pdo->exec('DELETE FROM ticket_attachments'); $pdo->exec('DELETE FROM ticket_threads'); $pdo->exec('DELETE FROM ticket_ratings'); $pdo->exec('DELETE FROM tickets'); $pdo->exec('DELETE FROM users WHERE id NOT IN (SELECT DISTINCT user_id FROM tickets WHERE user_id IS NOT NULL)'); } @unlink($stateDir . '/entry_map.jsonl'); audit_log($staff['id'], 'Import-Reset: ' . $type); import_json(['ok' => true]); } // ── Import-Schritt ───────────────────────────────────────────────────────── if (($_POST['action'] ?? '') === 'import_chunk') { import_ajax_guard(); $pass = (string) ($_POST['pass'] ?? ''); $offset = (int) ($_POST['offset'] ?? 0); if (!in_array($pass, ['users', 'structure', 'tickets', 'threads', 'attachments'], true)) import_json(['ok' => false, 'error' => 'Unbekannter Durchgang']); if ($pass === 'threads' && $offset === 0) @unlink($stateDir . '/entry_map.jsonl'); try { $res = osticket_import_step($pdo, $stateDir, $pass, $offset, (string) ($_SESSION['import_sql_path'] ?? '')); } catch (Throwable $e) { import_json(['ok' => false, 'error' => $e->getMessage()]); } $importWhat = $_SESSION['import_what'] ?? []; if ($res['done'] && $pass === end($importWhat)) { $tmp = $_SESSION['import_sql_path'] ?? ''; if ($tmp && file_exists($tmp) && str_starts_with($tmp, $stateDir)) @unlink($tmp); osticket_state_clear($stateDir); unset($_SESSION['import_sql_path'], $_SESSION['import_what'], $_SESSION['import_filename']); audit_log($staff['id'], 'osTicket Import abgeschlossen'); } import_json(['ok' => true] + $res); } // ── Seite ────────────────────────────────────────────────────────────────── $step = $_GET['step'] ?? '1'; $sqlPath = $_SESSION['import_sql_path'] ?? ''; $filename = $_SESSION['import_filename'] ?? ''; $fileSize = $sqlPath && file_exists($sqlPath) ? filesize($sqlPath) : 0; $pageTitle = 'osTicket Import'; $activePage = 'settings'; require __DIR__ . '/_header.php'; ?>

osTicket Import

← Einstellungen
'Hochladen','analyze'=>'Analysieren','2'=>'Optionen','run'=>'Import'] as $s=>$l): ?>

SQL-Datei hochladen oder Pfad angeben

Unterstützt werden SQL-Dumps von osTicket 1.10 bis 1.18 (phpMyAdmin-Export oder mysqldump, auch mit anderem Tabellenpräfix als ost_). Importiert werden Kunden, Abteilungen, Hilfethemen, Tickets, Nachrichten und Anhänge.

Option 1: Datei in den DeskHub-Ordner legen (für große Dateien empfohlen)


Option 2: Direkt hochladen (in Teilen von MB, passend zum Limit dieses Servers)

Analysiere: ( MB)

Liest die Datei in 4-MB-Blöcken. Bei sehr großen Dateien (Anhänge, Sitzungsdaten) kann das einige Minuten dauern.

Starte...
'Kunden','tickets'=>'Tickets','threads'=>'Nachrichten','departments'=>'Abteilungen','topics'=>'Hilfethemen','attachments'=>'Anhänge'] as $k=>$l): ?>
0

Was soll importiert werden?

'Kunden','tickets'=>'Tickets','threads'=>'Nachrichten','departments'=>'Abteilungen','topics'=>'Hilfethemen','attachments'=>'Anhänge'] as $k=>$l): ?>
['Kunden', 'Name und E-Mail-Adresse'], 'structure' => ['Abteilungen und Hilfethemen', 'Fehlende werden angelegt, gleichnamige vorhandene wiederverwendet. Tickets landen damit in der richtigen Abteilung.'], 'tickets' => ['Tickets', 'Betreff, Nummer, Status, Priorität, Abteilung, Hilfethema, zugewiesener Mitarbeiter (über gleiche E-Mail-Adresse), Zeitstempel. Gelöschte Tickets werden ausgelassen.'], 'threads' => ['Nachrichten', 'Kundennachrichten, Antworten, interne Notizen und Mitlesende'], 'attachments' => ['Anhänge (' . round($counts['attachment_bytes'] / 1048576, 1) . ' MB)', 'Dateien zu den Nachrichten, bis ' . round(OSTICKET_ATTACHMENT_MAX_BYTES / 1048576) . ' MB je Datei. Setzt die Nachrichten voraus.'], ] as $v => [$lb, $desc]): ?>

Der Import kann gefahrlos wiederholt werden: Vorhandene Kunden und Ticketnummern werden übersprungen, bereits importierte Nachrichten nicht doppelt angelegt. Vorher ein Backup erstellen ist trotzdem empfehlenswert.

Neu hochladen

Import läuft...

Seite nicht schließen.

'Kunden','structure'=>'Abteilungen und Hilfethemen','tickets'=>'Tickets','threads'=>'Nachrichten','attachments'=>'Anhänge'] as $p=>$l): if(!in_array($p,$importWhat)) continue; ?>
Wartend...