= $now;
}
}
fclose($h);
}
}
$txt = __DIR__ . '/valid_tokens.txt';
if (file_exists($txt)) {
$arr = file($txt, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
if ($arr && in_array($token, $arr, true)) return true;
}
return false;
}
// -----------------------------
// Hauptlogik
// -----------------------------
try {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(["error" => "Nur POST erlaubt"]);
exit;
}
// Token aus multipart oder Authorization: Bearer
$token = $_POST['token'] ?? null;
if (!$token) {
$auth = $_SERVER['HTTP_AUTHORIZATION'] ?? ($_SERVER['Authorization'] ?? '');
if (stripos($auth, 'Bearer ') === 0) {
$token = substr($auth, 7);
}
}
if (!$token || !token_is_valid($token)) {
http_response_code(403);
echo json_encode(["error" => "Ungültiger Token"]);
exit;
}
// Upload entgegennehmen (multipart 'file' oder raw body)
$uploadedFilePath = null;
if (!empty($_FILES['file']) && is_uploaded_file($_FILES['file']['tmp_name'])) {
$uploadedFilePath = $_FILES['file']['tmp_name'];
} else {
$raw = file_get_contents('php://input');
if ($raw !== false && strlen($raw) > 0) {
$tmp = tempnam(sys_get_temp_dir(), 'upl_');
file_put_contents($tmp, $raw);
$uploadedFilePath = $tmp;
}
}
if (!$uploadedFilePath) {
http_response_code(400);
echo json_encode(["error" => "Keine Datei gesendet"]);
exit;
}
// Payload mit SESSION-Key (aus Token via HKDF) entschlüsseln
$encData = file_get_contents($uploadedFilePath);
if ($encData === false) {
http_response_code(500);
echo json_encode(["error" => "Fehler beim Lesen der hochgeladenen Datei"]);
exit;
}
$sessionKey = hkdf_session_key_from_token($token);
$jsonData = null;
try {
$jsonData = aes256_cbc_decrypt_bytes($encData, $sessionKey);
} catch (Throwable $e) {
// Fallback: akzeptiere Plain-JSON (Kompatibilität)
$maybeJson = trim($encData);
if ($maybeJson === '' || json_decode($maybeJson, true) === null) {
log_msg("Entschlüsselung fehlgeschlagen und kein valides JSON. raw len=" . strlen($encData));
http_response_code(400);
echo json_encode(["error" => "Ungültige verschlüsselte Datei oder kein JSON"]);
exit;
}
$jsonData = $maybeJson;
}
$data = json_decode($jsonData, true);
if (!is_array($data)) {
http_response_code(400);
echo json_encode(["error" => "Ungültiges JSON"]);
exit;
}
// Ziel: MASTER-verschlüsselte DB speichern
$dbPath = __DIR__ . '/uploads/questionnaire_database';
if (!is_dir(dirname($dbPath))) {
if (!mkdir(dirname($dbPath), 0755, true) && !is_dir(dirname($dbPath))) {
throw new Exception("Konnte Upload-Ordner nicht erstellen");
}
}
// Lock, damit keine parallelen Writes kollidieren
$lockFile = __DIR__ . '/uploads/.qdb_lock';
$lockFp = fopen($lockFile, 'c');
if ($lockFp === false) throw new Exception("Konnte Lock-Datei nicht öffnen");
if (!flock($lockFp, LOCK_EX)) { fclose($lockFp); throw new Exception("Konnte Lock nicht setzen"); }
$tmpDb = null;
$tmpEncrypted = null;
try {
$tmpDb = tempnam(sys_get_temp_dir(), 'qdb_');
$masterKey = get_master_key_bytes();
// Bestehende MASTER-verschlüsselte DB entschlüsseln oder neue DB mit Schema anlegen
if (file_exists($dbPath) && is_file($dbPath)) {
$storedEnc = @file_get_contents($dbPath);
if ($storedEnc === false) throw new Exception("Konnte gespeicherte DB nicht lesen");
$decrypted = aes256_cbc_decrypt_bytes($storedEnc, $masterKey);
if (file_put_contents($tmpDb, $decrypted) === false) throw new Exception("Konnte temporäre DB nicht schreiben");
} else {
$pdo = new PDO("sqlite:$tmpDb");
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->exec("PRAGMA foreign_keys = ON;");
$pdo->exec("
CREATE TABLE IF NOT EXISTS clients (
clientCode TEXT NOT NULL DEFAULT 'undefined' PRIMARY KEY
);
CREATE TABLE IF NOT EXISTS questionnaires (
id TEXT NOT NULL DEFAULT 'undefined' PRIMARY KEY
);
CREATE TABLE IF NOT EXISTS questions (
questionId TEXT NOT NULL DEFAULT 'undefined' PRIMARY KEY,
questionnaireId TEXT NOT NULL,
question TEXT NOT NULL DEFAULT '',
FOREIGN KEY(questionnaireId) REFERENCES questionnaires(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS answers (
clientCode TEXT NOT NULL,
questionId TEXT NOT NULL,
answerValue TEXT NOT NULL DEFAULT '',
PRIMARY KEY (clientCode, questionId),
FOREIGN KEY(clientCode) REFERENCES clients(clientCode) ON DELETE CASCADE,
FOREIGN KEY(questionId) REFERENCES questions(questionId) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS completed_questionnaires (
clientCode TEXT NOT NULL,
questionnaireId TEXT NOT NULL,
timestamp INTEGER NOT NULL,
isDone INTEGER NOT NULL,
sumPoints INTEGER,
PRIMARY KEY (clientCode, questionnaireId),
FOREIGN KEY(clientCode) REFERENCES clients(clientCode) ON DELETE CASCADE,
FOREIGN KEY(questionnaireId) REFERENCES questionnaires(id) ON DELETE CASCADE
);
");
$pdo = null;
}
// Einspielen der Daten
$db = new PDO("sqlite:$tmpDb");
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$db->exec("PRAGMA foreign_keys = ON;");
$db->beginTransaction();
// --- Sammeln, welche IDs/Paarungen betroffen sind ---
$questionnaireIds = []; // set[string] questionnaireId => true
$clientQuestionPairs = []; // set["client|qid"] => true
if (!empty($data['questionnaires'])) {
foreach ($data['questionnaires'] as $q) {
if (isset($q['id']) && $q['id'] !== null && $q['id'] !== '') {
$questionnaireIds[$q['id']] = true;
}
}
}
if (!empty($data['questions'])) {
foreach ($data['questions'] as $q) {
if (isset($q['questionnaireId']) && $q['questionnaireId'] !== null && $q['questionnaireId'] !== '') {
$questionnaireIds[$q['questionnaireId']] = true;
}
}
}
if (!empty($data['completed_questionnaires'])) {
foreach ($data['completed_questionnaires'] as $c) {
if (isset($c['questionnaireId']) && $c['questionnaireId'] !== null && $c['questionnaireId'] !== '') {
$questionnaireIds[$c['questionnaireId']] = true;
}
if (isset($c['clientCode']) && isset($c['questionnaireId'])) {
$client = ($c['clientCode'] === null || $c['clientCode'] === '') ? 'undefined' : $c['clientCode'];
$qid = ($c['questionnaireId'] === null || $c['questionnaireId'] === '') ? 'undefined' : $c['questionnaireId'];
$clientQuestionPairs["$client|$qid"] = true;
}
}
}
if (!empty($data['answers'])) {
foreach ($data['answers'] as $a) {
if (isset($a['clientCode'])) {
$client = ($a['clientCode'] === null || $a['clientCode'] === '') ? 'undefined' : $a['clientCode'];
$qid = null;
if (isset($a['questionId']) && is_string($a['questionId'])) {
$parts = explode('-', $a['questionId'], 2);
if (count($parts) >= 1) {
$potential = $parts[0];
if ($potential !== '') $qid = $potential;
}
}
if ($qid !== null) {
$questionnaireIds[$qid] = true;
$clientQuestionPairs["$client|$qid"] = true;
}
}
}
}
if (!empty($data['clients']) && !empty($data['questionnaires'])) {
foreach ($data['clients'] as $c) {
if (!isset($c['clientCode'])) continue;
$client = ($c['clientCode'] === null || $c['clientCode'] === '') ? 'undefined' : $c['clientCode'];
foreach ($data['questionnaires'] as $q) {
if (!isset($q['id'])) continue;
$qid = ($q['id'] === null || $q['id'] === '') ? 'undefined' : $q['id'];
$clientQuestionPairs["$client|$qid"] = true;
}
}
}
// --- Löschungen (zuerst abhängige Tabellen) ---
$delAnswersByPairStmt = $db->prepare("
DELETE FROM answers
WHERE clientCode = :clientCode
AND questionId IN (SELECT questionId FROM questions WHERE questionnaireId = :questionnaireId)
");
$delCompletedByPairStmt = $db->prepare("
DELETE FROM completed_questionnaires
WHERE clientCode = :clientCode
AND questionnaireId = :questionnaireId
");
foreach ($clientQuestionPairs as $pair => $_) {
list($clientCode, $qid) = explode('|', $pair, 2);
try {
$delAnswersByPairStmt->execute([
':clientCode' => $clientCode,
':questionnaireId' => $qid
]);
$delCompletedByPairStmt->execute([
':clientCode' => $clientCode,
':questionnaireId' => $qid
]);
log_msg("Deleted answers & completed for client='$clientCode' questionnaire='$qid'");
} catch (Exception $e) {
log_msg("Delete error for pair $pair: " . $e->getMessage());
}
}
if (!empty($questionnaireIds)) {
$delQuestionsStmt = $db->prepare("DELETE FROM questions WHERE questionnaireId = :questionnaireId");
foreach ($questionnaireIds as $qid => $_) {
try {
$delQuestionsStmt->execute([':questionnaireId' => $qid]);
log_msg("Deleted questions for questionnaireId='$qid'");
} catch (Exception $e) {
log_msg("Delete questions error for $qid: " . $e->getMessage());
}
}
}
// --- Inserts/Upserts ---
if (!empty($data['clients'])) {
$stmt = $db->prepare("INSERT OR IGNORE INTO clients (clientCode) VALUES (:clientCode)");
foreach ($data['clients'] as $client) {
if (!isset($client['clientCode'])) continue;
$val = $client['clientCode'] === null || $client['clientCode'] === '' ? 'undefined' : $client['clientCode'];
$stmt->execute([':clientCode' => $val]);
}
}
if (!empty($data['questionnaires'])) {
$stmt = $db->prepare("INSERT OR IGNORE INTO questionnaires (id) VALUES (:id)");
foreach ($data['questionnaires'] as $q) {
if (!isset($q['id'])) continue;
$val = $q['id'] === null || $q['id'] === '' ? 'undefined' : $q['id'];
$stmt->execute([':id' => $val]);
}
}
if (!empty($data['questions'])) {
$stmt = $db->prepare("
INSERT OR IGNORE INTO questions (questionId, questionnaireId, question)
VALUES (:questionId, :questionnaireId, :question)
");
foreach ($data['questions'] as $q) {
if (!isset($q['questionId'])) continue;
$questionId = $q['questionId'] === null || $q['questionId'] === '' ? 'undefined' : $q['questionId'];
$questionnaireId = $q['questionnaireId'] === null || $q['questionnaireId'] === '' ? 'undefined' : $q['questionnaireId'];
$questionText = $q['question'] ?? '';
$stmt->execute([
':questionId' => $questionId,
':questionnaireId' => $questionnaireId,
':question' => $questionText
]);
}
}
if (!empty($data['answers'])) {
$stmt = $db->prepare("
INSERT OR IGNORE INTO answers (clientCode, questionId, answerValue)
VALUES (:clientCode, :questionId, :answerValue)
");
foreach ($data['answers'] as $a) {
if (!isset($a['clientCode']) || !isset($a['questionId'])) continue;
$clientCode = $a['clientCode'] === null || $a['clientCode'] === '' ? 'undefined' : $a['clientCode'];
$questionId = $a['questionId'] === null || $a['questionId'] === '' ? 'undefined' : $a['questionId'];
$answerValue = array_key_exists('answerValue', $a) && $a['answerValue'] !== null ? $a['answerValue'] : '';
$stmt->execute([
':clientCode' => $clientCode,
':questionId' => $questionId,
':answerValue' => $answerValue
]);
}
}
if (!empty($data['completed_questionnaires'])) {
$stmt = $db->prepare("
INSERT OR REPLACE INTO completed_questionnaires (clientCode, questionnaireId, timestamp, isDone, sumPoints)
VALUES (:clientCode, :questionnaireId, :timestamp, :isDone, :sumPoints)
");
foreach ($data['completed_questionnaires'] as $c) {
if (!isset($c['clientCode']) || !isset($c['questionnaireId'])) continue;
$clientCode = $c['clientCode'] === null || $c['clientCode'] === '' ? 'undefined' : $c['clientCode'];
$questionnaireId = $c['questionnaireId'] === null || $c['questionnaireId'] === '' ? 'undefined' : $c['questionnaireId'];
$timestamp = isset($c['timestamp']) ? (int)$c['timestamp'] : time();
$isDone = !empty($c['isDone']) ? 1 : 0;
$sumPoints = array_key_exists('sumPoints', $c) ? ($c['sumPoints'] === null ? null : (int)$c['sumPoints']) : null;
$stmt->execute([
':clientCode' => $clientCode,
':questionnaireId' => $questionnaireId,
':timestamp' => $timestamp,
':isDone' => $isDone,
':sumPoints' => $sumPoints
]);
}
}
$db->commit();
$db = null;
// MASTER-verschlüsselt speichern (atomar via temp + rename)
$plainDb = file_get_contents($tmpDb);
if ($plainDb === false) throw new Exception("Konnte tmp DB nicht lesen");
$enc = aes256_cbc_encrypt_bytes($plainDb, $masterKey);
$tmpEncrypted = tempnam(dirname($dbPath), 'enc_qdb_');
if (file_put_contents($tmpEncrypted, $enc) === false) throw new Exception("Konnte verschlüsselte DB nicht schreiben");
if (!@rename($tmpEncrypted, $dbPath)) {
if (!@copy($tmpEncrypted, $dbPath) || !@unlink($tmpEncrypted)) {
throw new Exception("Konnte verschlüsselte DB nicht speichern (rename/copy fehlgeschlagen)");
}
}
@chmod($dbPath, 0644);
// Cleanup
@unlink($uploadedFilePath);
@unlink($tmpDb);
flock($lockFp, LOCK_UN);
fclose($lockFp);
echo json_encode(["success" => true, "message" => "Delta erfolgreich eingespielt und DB gespeichert"]);
exit;
} catch (Throwable $inner) {
// Cleanup bei Fehler
@flock($lockFp, LOCK_UN);
@fclose($lockFp);
@unlink($tmpDb ?? '');
@unlink($tmpEncrypted ?? '');
@unlink($uploadedFilePath ?? '');
log_msg("Inner exception: " . $inner->getMessage());
http_response_code(500);
echo json_encode(["error" => "Fehler beim Speichern", "message" => $inner->getMessage()]);
exit;
}
} catch (Throwable $e) {
log_msg("Top-level exception: " . $e->getMessage());
http_response_code(500);
echo json_encode(["error" => "Fehler", "message" => $e->getMessage()]);
exit;
}