0, 'path' => '/', 'secure' => $installHttps, 'httponly' => true, 'samesite' => 'Lax']); session_start(); define('ROOT_DIR', dirname(__DIR__)); define('LOCK_FILE', __DIR__ . '/.installed'); require_once ROOT_DIR . '/database/sqlite_schema.php'; /** Gleiche Regeln wie im Programm (validate_password_strength), damit das Admin-Konto nicht schwächer ist als alle anderen. */ function install_password_error(string $p): ?string { if (strlen($p) < 10) return 'Das Admin-Passwort muss mindestens 10 Zeichen lang sein.'; if (!preg_match('/[A-Z]/', $p)) return 'Das Admin-Passwort muss mindestens einen Großbuchstaben enthalten.'; if (!preg_match('/[a-z]/', $p)) return 'Das Admin-Passwort muss mindestens einen Kleinbuchstaben enthalten.'; if (!preg_match('/[0-9]/', $p)) return 'Das Admin-Passwort muss mindestens eine Ziffer enthalten.'; if (!preg_match('/[^A-Za-z0-9]/', $p)) return 'Das Admin-Passwort muss mindestens ein Sonderzeichen enthalten.'; return null; } /** Verständliche Meldung zu einem Datenbankfehler (mit dem technischen Text dahinter). */ function install_db_error(Throwable $e): string { $code = (int) ($e instanceof PDOException ? ($e->errorInfo[1] ?? $e->getCode()) : 0); $hint = match (true) { $code === 1045 => 'Benutzername oder Passwort der Datenbank stimmen nicht.', $code === 1049 => 'Die Datenbank existiert nicht. Legen Sie sie zuerst (leer) an, z. B. im Hosting-Bereich.', $code === 1044 => 'Der Datenbank-Benutzer hat keine Rechte auf diese Datenbank.', $code === 2002 || $code === 2006 => 'Der Datenbank-Server ist nicht erreichbar. Prüfen Sie Host (und Port, z. B. localhost:3306).', $code === 2054 => 'Der Datenbank-Server verlangt eine Anmeldemethode, die diese PHP-Version nicht unterstützt.', default => 'Prüfen Sie Host, Datenbankname, Benutzer und Passwort.', }; return 'Datenbankverbindung fehlgeschlagen. ' . $hint . ' (Technisch: ' . $e->getMessage() . ')'; } /** * Schreibt die Konfiguration so, dass sie sicher gilt: auch wenn schon eine (alte, z. B. mitgelieferte) config.php liegt, * die dem Webserver nicht gehört oder nicht beschreibbar ist, und mit Löschen des PHP-Zwischenspeichers (OPcache), * damit nicht weiter die alte Version geladen wird. */ function install_write_config(string $path, string $content): bool { $ok = @file_put_contents($path, $content) !== false; if (!$ok && is_file($path)) { @chmod($path, 0644); $ok = @file_put_contents($path, $content) !== false; if (!$ok && @unlink($path)) $ok = @file_put_contents($path, $content) !== false; // Datei gehört einem anderen Benutzer, der Ordner aber nicht } if ($ok) { clearstatcache(true, $path); if (function_exists('opcache_invalidate')) @opcache_invalidate($path, true); } return $ok; } /** Liest die geschriebene Konfiguration frisch ein und prüft, dass genau die eingegebenen Daten darin stehen. Gibt null oder eine Fehlermeldung zurück. */ function install_verify_config(string $path, array $expected): ?string { if (function_exists('opcache_invalidate')) @opcache_invalidate($path, true); $onDisk = (string) @file_get_contents($path); if (!str_contains($onDisk, var_export((string) $expected['app_key'], true))) return 'Die Konfigurationsdatei enthält nicht die neuen Daten. Prüfen Sie die Schreibrechte auf config/config.php.'; $loaded = @include $path; $db = is_array($loaded) ? (array) ($loaded['db'] ?? []) : []; foreach (['driver', 'host', 'name', 'user', 'pass'] as $k) { if ((string) ($db[$k] ?? '') !== (string) $expected['db'][$k]) { return 'Die Konfiguration wurde geschrieben, PHP liefert aber noch eine ältere Version (Zwischenspeicher/OPcache des Servers). Laden Sie PHP neu (Hosting-Oberfläche) oder warten Sie einige Minuten und starten Sie die Installation erneut.'; } } return null; } function h(?string $s): string { return htmlspecialchars($s ?? '', ENT_QUOTES, 'UTF-8'); } // URL der aktuellen Installation automatisch erkennen $detectedUrl = (function(): string { $scheme = $GLOBALS['installHttps'] ? 'https' : 'http'; $host = $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'] ?? 'localhost'; // install/ vom Pfad entfernen → Root-URL $path = rtrim(dirname($_SERVER['SCRIPT_NAME'] ?? ''), '/\\'); $path = preg_replace('#/install$#', '', $path); return rtrim($scheme . '://' . $host . $path, '/'); })(); /** * Fehlt die Sperrdatei (z. B. weil Dateien neu hochgeladen wurden), darf der Assistent trotzdem keine bestehende * Installation überschreiben: Gibt es schon eine Konfiguration mit Datenbank und Mitarbeiterkonten, gilt sie als installiert. */ $existingInstallation = (function (): bool { $cfgFile = ROOT_DIR . '/config/config.php'; if (!is_file($cfgFile)) return false; try { $cfg = (array) (require $cfgFile); $db = (array) ($cfg['db'] ?? []); if (($db['driver'] ?? 'sqlite') === 'mysql') { $pdo = new PDO('mysql:host=' . ($db['host'] ?? 'localhost') . ';dbname=' . ($db['name'] ?? '') . ';charset=utf8mb4', (string) ($db['user'] ?? ''), (string) ($db['pass'] ?? ''), [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_TIMEOUT => 3]); } else { $path = (string) ($db['sqlite_path'] ?? ''); if ($path === '' || !is_file($path)) return false; $pdo = new PDO('sqlite:' . $path, null, null, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]); } return (int) $pdo->query('SELECT COUNT(*) FROM staff')->fetchColumn() > 0; } catch (Throwable $e) { return false; // keine nutzbare Datenbank: Erstinstallation ist in Ordnung } })(); $alreadyInstalled = file_exists(LOCK_FILE) || $existingInstallation; $errors = []; $success = false; $manualConfig = null; // --------------------------------------------------------------- // Anforderungsprüfung // --------------------------------------------------------------- // Ordner anlegen, die für den Betrieb nötig sind (falls beim Hochladen leer weggelassen) foreach (['uploads', 'backups', 'plugins', 'app_downloads', 'config'] as $__dir) { if (!is_dir(ROOT_DIR . '/' . $__dir)) @mkdir(ROOT_DIR . '/' . $__dir, 0755, true); } $checks = [ ['label' => 'PHP-Version 8.2 oder neuer (gefunden: ' . PHP_VERSION . ')', 'ok' => version_compare(PHP_VERSION, '8.2.0', '>=')], ['label' => 'PDO-Erweiterung', 'ok' => extension_loaded('pdo')], ['label' => 'PDO MySQL-Treiber (für Produktivbetrieb)', 'ok' => extension_loaded('pdo_mysql'), 'optional' => true], ['label' => 'PDO SQLite-Treiber (optional, für lokale Tests)', 'ok' => extension_loaded('pdo_sqlite'), 'optional' => true], ['label' => 'Mindestens ein Datenbanktreiber (MySQL oder SQLite)', 'ok' => extension_loaded('pdo_mysql') || extension_loaded('pdo_sqlite')], ['label' => 'mbstring-Erweiterung', 'ok' => extension_loaded('mbstring')], ['label' => 'openssl-Erweiterung (Verschlüsselung der Zugangsdaten, Signaturen)', 'ok' => extension_loaded('openssl')], ['label' => 'json-Erweiterung', 'ok' => extension_loaded('json')], ['label' => 'curl-Erweiterung (optional: Webhooks, Plugin-Katalog, Updates)', 'ok' => extension_loaded('curl'), 'optional' => true], ['label' => 'zip-Erweiterung (optional: Plugins, Themes, Update per ZIP, Backups)', 'ok' => extension_loaded('zip'), 'optional' => true], ['label' => 'IMAP-Erweiterung (optional, nur für E-Mail-zu-Ticket)', 'ok' => extension_loaded('imap'), 'optional' => true], ['label' => 'Verzeichnis config/ ist beschreibbar', 'ok' => is_writable(ROOT_DIR . '/config')], ['label' => 'Verzeichnis uploads/ ist beschreibbar', 'ok' => is_writable(ROOT_DIR . '/uploads')], ['label' => 'Verzeichnis database/ ist beschreibbar (nur für SQLite nötig)', 'ok' => is_writable(ROOT_DIR . '/database'), 'optional' => true], ['label' => 'Verzeichnis backups/ ist beschreibbar (für Backups)', 'ok' => is_writable(ROOT_DIR . '/backups'), 'optional' => true], ['label' => 'HTTPS (empfohlen: Anmeldedaten sonst unverschlüsselt)', 'ok' => $installHttps, 'optional' => true], ]; $requiredOk = true; foreach ($checks as $c) { if (empty($c['optional']) && !$c['ok']) { $requiredOk = false; } } // --------------------------------------------------------------- // Installation durchführen // --------------------------------------------------------------- if ($_SERVER['REQUEST_METHOD'] === 'POST' && !$alreadyInstalled && $requiredOk) { if (empty($_SESSION['install_csrf']) || !hash_equals($_SESSION['install_csrf'], $_POST['csrf_token'] ?? '')) { $errors[] = 'Ungültige Anfrage (Sitzung abgelaufen). Bitte die Seite neu laden und erneut versuchen.'; } else { $driver = ($_POST['driver'] ?? '') === 'mysql' ? 'mysql' : 'sqlite'; $siteName = trim($_POST['site_name'] ?? '') ?: 'DeskHub Support'; $siteUrl = rtrim(trim($_POST['site_url'] ?? ''), '/'); $adminName = trim($_POST['admin_name'] ?? ''); $adminEmail = trim($_POST['admin_email'] ?? ''); $adminPass = $_POST['admin_password'] ?? ''; $adminPass2 = $_POST['admin_password2'] ?? ''; if ($siteUrl === '') $errors[] = 'Bitte die URL der Installation angeben (z.B. https://support.firma.de).'; if ($adminName === '') $errors[] = 'Bitte einen Namen für das Admin-Konto angeben.'; if (!filter_var($adminEmail, FILTER_VALIDATE_EMAIL)) $errors[] = 'Bitte eine gültige Admin-E-Mail-Adresse angeben.'; if (($pwError = install_password_error($adminPass)) !== null) $errors[] = $pwError; if ($adminPass !== $adminPass2) $errors[] = 'Die beiden Passwörter stimmen nicht überein.'; if (!extension_loaded('pdo_' . $driver)) $errors[] = "Die PHP-Erweiterung pdo_{$driver} ist nicht installiert."; $dbConfig = [ 'driver' => $driver, 'host' => 'localhost', 'name' => 'deskhub', 'user' => 'deskhub_user', 'pass' => 'change_me', 'sqlite_path' => ROOT_DIR . '/database/deskhub.sqlite', ]; if ($driver === 'mysql') { $dbConfig['host'] = trim($_POST['db_host'] ?? 'localhost') ?: 'localhost'; $dbConfig['name'] = trim($_POST['db_name'] ?? ''); $dbConfig['user'] = trim($_POST['db_user'] ?? ''); $dbConfig['pass'] = $_POST['db_pass'] ?? ''; if ($dbConfig['name'] === '' || $dbConfig['user'] === '') { $errors[] = 'Bitte Datenbankname und Datenbankbenutzer angeben.'; } } $pdo = null; if (!$errors) { try { if ($driver === 'mysql') { $dsn = sprintf('mysql:host=%s;dbname=%s;charset=utf8mb4', $dbConfig['host'], $dbConfig['name']); $pdo = new PDO($dsn, $dbConfig['user'], $dbConfig['pass']); } else { // Nur bei gewähltem SQLite wird eine Datenbank-Datei angelegt, und nur für diese Installation: // Bricht sie ab, wird die halbfertige Datei am Ende des Aufrufs wieder entfernt. @unlink($dbConfig['sqlite_path']); $sqlitePath = $dbConfig['sqlite_path']; register_shutdown_function(function () use (&$success, &$pdo, &$stmt, $sqlitePath) { if ($success) return; // Verbindung und Statements schließen, sonst lässt sich die Datei (vor allem unter Windows) nicht löschen $stmt = null; $pdo = null; gc_collect_cycles(); foreach ([$sqlitePath, $sqlitePath . '-wal', $sqlitePath . '-shm'] as $leftover) @unlink($leftover); }); $pdo = new PDO('sqlite:' . $dbConfig['sqlite_path']); $pdo->exec('PRAGMA foreign_keys = ON;'); } $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); } catch (Throwable $e) { $errors[] = install_db_error($e); } } if (!$errors && $pdo) { try { $sql = file_get_contents(ROOT_DIR . '/database/schema.sql'); if ($driver === 'sqlite') { $sql = convert_schema_to_sqlite($sql); } $pdo->exec($sql); // Vordefiniertes Demo-Admin-Konto entfernen und durch das hier // eingegebene, echte Admin-Konto ersetzen. $pdo->prepare('DELETE FROM staff WHERE email IN (?, ?)')->execute(['admin@example.com', $adminEmail]); $stmt = $pdo->prepare( 'INSERT INTO staff (name, email, password_hash, role, department_id, is_active, created_at) VALUES (?,?,?,?,?,1,?)' ); $stmt->execute([$adminName, $adminEmail, password_hash($adminPass, PASSWORD_DEFAULT), 'admin', 1, date('Y-m-d H:i:s')]); // ── Initiale Einstellungen in die DB seeden ─────────────────────── // Alle Kerneinstellungen sofort in der settings-Tabelle anlegen, // damit sie Updates überleben (config.php wird beim Update nie // überschrieben, aber so sind die Werte auch bei Neuinstallationen // sofort über get_setting() abrufbar). // Sicherer, zufälliger App-Key – direkt hier generieren, damit er // sowohl in die DB (überlebt Updates) als auch in config.php geschrieben wird. $appKey = bin2hex(random_bytes(32)); $installSettings = [ 'app_key' => $appKey, // ← in DB: überlebt Updates 'display_name' => $siteName, 'site_url' => $siteUrl, 'timezone' => 'Europe/Berlin', 'smtp_from_email' => $adminEmail, 'smtp_from_name' => $siteName, 'mail_enabled' => '0', 'helpdesk_status' => 'online', 'kb_enabled' => '1', 'ticket_ratings_enabled' => '1', 'staff_session_timeout' => '30', 'retention_enabled' => '0', 'retention_months' => '24', 'ip_anonymize' => '0', 'upload_allowed_extensions' => 'png, jpg, jpeg, gif, pdf, txt, log, docx, xlsx, zip', 'upload_max_size_mb' => '10', ]; $upsertSql = $driver === 'sqlite' ? 'INSERT OR REPLACE INTO settings (setting_key, setting_value) VALUES (?, ?)' : 'INSERT INTO settings (setting_key, setting_value) VALUES (?, ?) ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)'; foreach ($installSettings as $k => $v) { $pdo->prepare($upsertSql)->execute([$k, $v]); } // Alle vorhandenen Migrationsdateien sofort als "eingespielt" markieren, // da das Schema gerade frisch erstellt wurde. $migrTable = $driver === 'sqlite' ? "CREATE TABLE IF NOT EXISTS schema_migrations (migration VARCHAR(255) PRIMARY KEY, applied_at DATETIME NOT NULL DEFAULT (datetime('now')))" : "CREATE TABLE IF NOT EXISTS schema_migrations (migration VARCHAR(255) PRIMARY KEY, applied_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"; $pdo->exec($migrTable); $insertMigr = $driver === 'sqlite' ? 'INSERT OR IGNORE INTO schema_migrations (migration) VALUES (?)' : 'INSERT IGNORE INTO schema_migrations (migration) VALUES (?)'; foreach (glob(ROOT_DIR . '/database/migrations/*.sql') ?: [] as $mf) { $pdo->prepare($insertMigr)->execute([basename($mf)]); } } catch (Throwable $e) { $errors[] = 'Fehler beim Einrichten der Datenbank: ' . $e->getMessage(); } } if (!$errors) { $configArray = [ 'app_key' => $appKey, 'db' => $dbConfig, 'site' => [ 'name' => $siteName, 'url' => $siteUrl, 'admin_email' => $adminEmail, 'timezone' => 'Europe/Berlin', ], 'mail' => [ 'enabled' => false, 'from_email' => $adminEmail, 'from_name' => $siteName, ], 'imap' => [ 'enabled' => false, 'mailbox' => '{imap.example.com:993/imap/ssl}INBOX', 'user' => 'support@example.com', 'pass' => 'change_me', 'default_department_id' => 1, 'default_priority_id' => 2, ], 'uploads' => [ 'max_size_bytes' => 10 * 1024 * 1024, 'allowed_extensions' => ['png','jpg','jpeg','gif','pdf','txt','log','docx','xlsx','zip'], 'path' => ROOT_DIR . '/uploads', ], ]; $php = "getMessage(); } } } } } } if (empty($_SESSION['install_csrf'])) { $_SESSION['install_csrf'] = bin2hex(random_bytes(32)); } ?> DeskHub – Installation
🔒

Bereits installiert

Eingerichtet am

Es gibt bereits eine Konfiguration mit Datenbank und Mitarbeiterkonten. Eine Neuinstallation würde diese Daten überschreiben und ist deshalb gesperrt.

⚠️ Bitte das Verzeichnis install/ vom Server löschen, um diese Seite zu deaktivieren.

Wirklich neu installieren? Erst ein Backup der Daten anlegen, dann config/config.php, install/.installed und die Datenbank (bei SQLite die Datei database/deskhub.sqlite) entfernen. Alle bisherigen Daten gehen dabei verloren.

✅

Installation abgeschlossen!

DeskHub wurde erfolgreich eingerichtet.


🗑️ Wichtig: Löschen Sie jetzt das Verzeichnis install/ vom Server, damit niemand die Installation wiederholen kann.
1 Systemvoraussetzungen
✗ Nicht alle Pflichtvoraussetzungen erfüllt. Bitte die rot markierten Punkte beheben und die Seite neu laden.
✗
Inhalt für config/config.php
Enthält Schlüssel und Datenbank-Passwort: nur auf dem Server speichern, nirgends weitergeben.
2 Konfiguration
🗄️ Datenbank
Die Datenbank muss bereits existieren (leer). Tabellen werden automatisch angelegt.
🌐 Website
Wird in E-Mail-Links verwendet. Kein abschließendes /
👤 Administrator-Konto
Mindestens 10 Zeichen mit Groß- und Kleinbuchstaben, Ziffer und Sonderzeichen.