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
DeskHub
Helpdesk & Ticketsystem
🎫
Ticket-Management
Abteilungen, Prioritäten, SLA, Regeln
📧
E-Mail-Integration
SMTP-Versand & IMAP-Abruf
👥
Kundenportal
Self-Service, Wissensdatenbank
📊
Berichte & Statistiken
SLA-Raten, Agenten-Performance
🔔
Benachrichtigungen
Push, Discord, Telegram
🔒
Sicherheit
2FA, verschlüsselte Zugangsdaten
DeskHub · Viper Network
🔒
Bereits installiert
Eingerichtet am = h((string) file_get_contents(LOCK_FILE)) ?>
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
= $ico ?>
= h($c['label']) ?>
✗
Nicht alle Pflichtvoraussetzungen erfüllt. Bitte die rot markierten Punkte beheben und die Seite neu laden.
✗ = h($err) ?>
Inhalt für config/config.php
Enthält Schlüssel und Datenbank-Passwort: nur auf dem Server speichern, nirgends weitergeben.