new system prototype

This commit is contained in:
2026-04-15 10:19:42 +02:00
parent e805f225bc
commit 034b108c7e
80 changed files with 12212 additions and 890 deletions

View File

@ -1,10 +1,7 @@
<?php
// /var/www/html/uploadDeltaTest5.php
// Vollständige, eigenständige Version einfach kopieren & einfügen.
require_once __DIR__ . '/db_init.php';
// -----------------------------
// Logging & PHP-Settings
// -----------------------------
error_reporting(E_ALL);
ini_set('display_errors', '0');
ini_set('log_errors', '1');
@ -15,99 +12,14 @@ function log_msg($msg) {
}
header('Content-Type: application/json; charset=UTF-8');
// -----------------------------
// Krypto-/Token-Helfer
// -----------------------------
function b64_or_ascii_to_32_bytes(string $s): string {
// Versuche Base64, sonst ASCII; pad/truncate auf 32
$b = base64_decode($s, true);
if ($b !== false && $b !== '') {
return str_pad(substr($b, 0, 32), 32, "\0");
}
return str_pad(substr($s, 0, 32), 32, "\0");
}
function get_master_key_bytes(): string {
// MASTER-KEY nur serverseitig (für gespeicherte DB)
// ENV: QDB_MASTER_KEY (Base64 oder ASCII). Fallback ist alter Key.
$env = getenv('QDB_MASTER_KEY');
if ($env && $env !== '') return b64_or_ascii_to_32_bytes($env);
return "12345678901234567890123456789012"; // Fallback (bitte in PROD durch ENV ersetzen!)
}
function hkdf_session_key_from_token(string $tokenHex, string $info = 'qdb-aes', int $len = 32): string {
// HKDF-SHA256: salt = 32x0x00, info = "qdb-aes"
$ikm = @hex2bin(trim($tokenHex));
if ($ikm === false) $ikm = $tokenHex; // falls bereits binär
$hashLen = 32;
$salt = str_repeat("\0", $hashLen);
$prk = hash_hmac('sha256', $ikm, $salt, true);
$okm = '';
$t = '';
$n = (int)ceil($len / $hashLen);
for ($i = 1; $i <= $n; $i++) {
$t = hash_hmac('sha256', $t . $info . chr($i), $prk, true);
$okm .= $t;
}
return substr($okm, 0, $len);
}
function aes256_cbc_encrypt_bytes(string $plain, string $key32): string {
$key = str_pad(substr($key32, 0, 32), 32, "\0");
$iv = random_bytes(16);
$cipher = openssl_encrypt($plain, 'AES-256-CBC', $key, OPENSSL_RAW_DATA, $iv);
if ($cipher === false) throw new Exception('openssl_encrypt failed');
return $iv . $cipher;
}
function aes256_cbc_decrypt_bytes(string $data, string $key32): string {
if (strlen($data) < 16) throw new Exception('cipher too short');
$key = str_pad(substr($key32, 0, 32), 32, "\0");
$iv = substr($data, 0, 16);
$ct = substr($data, 16);
$plain = openssl_decrypt($ct, 'AES-256-CBC', $key, OPENSSL_RAW_DATA, $iv);
if ($plain === false) throw new Exception('openssl_decrypt failed');
return $plain;
}
// Token-Validierung: bevorzugt tokens.jsonl mit exp, Fallback valid_tokens.txt
function token_is_valid(string $token): bool {
$now = time();
$jsonl = __DIR__ . '/tokens.jsonl';
if (file_exists($jsonl)) {
$h = fopen($jsonl, 'r');
if ($h) {
while (($line = fgets($h)) !== false) {
$j = json_decode($line, true);
if (!is_array($j)) continue;
if (($j['token'] ?? '') === $token) {
$exp = $j['exp'] ?? ($now + 1); // falls kein exp, akzeptieren
fclose($h);
return $exp >= $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"]);
echo json_encode(["error" => "Only POST allowed"]);
exit;
}
// Token aus multipart oder Authorization: Bearer
// Token from multipart field or Authorization: Bearer
$token = $_POST['token'] ?? null;
if (!$token) {
$auth = $_SERVER['HTTP_AUTHORIZATION'] ?? ($_SERVER['Authorization'] ?? '');
@ -115,13 +27,14 @@ try {
$token = substr($auth, 7);
}
}
if (!$token || !token_is_valid($token)) {
$tokenRec = $token ? token_get_record($token) : null;
if (!$tokenRec || !empty($tokenRec['temp'])) {
http_response_code(403);
echo json_encode(["error" => "Ungültiger Token"]);
echo json_encode(["error" => "Invalid token"]);
exit;
}
// Upload entgegennehmen (multipart 'file' oder raw body)
// Upload: multipart 'file' or raw body
$uploadedFilePath = null;
if (!empty($_FILES['file']) && is_uploaded_file($_FILES['file']['tmp_name'])) {
$uploadedFilePath = $_FILES['file']['tmp_name'];
@ -135,15 +48,15 @@ try {
}
if (!$uploadedFilePath) {
http_response_code(400);
echo json_encode(["error" => "Keine Datei gesendet"]);
echo json_encode(["error" => "No file sent"]);
exit;
}
// Payload mit SESSION-Key (aus Token via HKDF) entschlüsseln
// Decrypt payload with session key derived from token via HKDF
$encData = file_get_contents($uploadedFilePath);
if ($encData === false) {
http_response_code(500);
echo json_encode(["error" => "Fehler beim Lesen der hochgeladenen Datei"]);
echo json_encode(["error" => "Failed to read uploaded file"]);
exit;
}
$sessionKey = hkdf_session_key_from_token($token);
@ -151,12 +64,11 @@ try {
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));
log_msg("Decryption failed and not valid JSON. raw len=" . strlen($encData));
http_response_code(400);
echo json_encode(["error" => "Ungültige verschlüsselte Datei oder kein JSON"]);
echo json_encode(["error" => "Invalid encrypted file or not JSON"]);
exit;
}
$jsonData = $maybeJson;
@ -165,177 +77,119 @@ try {
$data = json_decode($jsonData, true);
if (!is_array($data)) {
http_response_code(400);
echo json_encode(["error" => "Ungültiges JSON"]);
echo json_encode(["error" => "Invalid 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");
}
}
// RBAC: coaches can only upload for their own clients
$role = $tokenRec['role'] ?? '';
$entityID = $tokenRec['entityID'] ?? '';
// 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;
[$pdo, $tmpDb, $lockFp] = qdb_open(true);
try {
$tmpDb = tempnam(sys_get_temp_dir(), 'qdb_');
$masterKey = get_master_key_bytes();
$pdo->beginTransaction();
// 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 ($role === 'coach' && !empty($data['client'])) {
foreach ($data['client'] 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;
$chk = $pdo->prepare("SELECT coachID FROM client WHERE clientCode = :cc");
$chk->execute([':cc' => $c['clientCode']]);
$existing = $chk->fetch(PDO::FETCH_ASSOC);
if ($existing && $existing['coachID'] !== $entityID) {
$pdo->rollBack();
qdb_discard($tmpDb, $lockFp);
http_response_code(403);
echo json_encode(["error" => "Not authorized to modify client " . $c['clientCode']]);
exit;
}
}
}
// --- 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
");
// --- Collect affected IDs ---
$questionnaireIds = [];
$clientQuestionPairs = [];
if (!empty($data['questionnaire'])) {
foreach ($data['questionnaire'] as $q) {
if (!empty($q['questionnaireID'])) $questionnaireIds[$q['questionnaireID']] = true;
}
}
if (!empty($data['question'])) {
foreach ($data['question'] as $q) {
if (!empty($q['questionnaireID'])) $questionnaireIds[$q['questionnaireID']] = true;
}
}
if (!empty($data['completed_questionnaire'])) {
foreach ($data['completed_questionnaire'] as $c) {
if (!empty($c['questionnaireID'])) $questionnaireIds[$c['questionnaireID']] = true;
if (isset($c['clientCode'], $c['questionnaireID'])) {
$cc = $c['clientCode'] ?: 'undefined';
$qid = $c['questionnaireID'] ?: 'undefined';
$clientQuestionPairs["$cc|$qid"] = true;
}
}
}
if (!empty($data['client_answer'])) {
foreach ($data['client_answer'] as $a) {
if (!isset($a['clientCode'])) continue;
$cc = $a['clientCode'] ?: 'undefined';
if (isset($a['questionID']) && is_string($a['questionID'])) {
$parts = explode('-', $a['questionID'], 2);
if ($parts[0] !== '') {
$questionnaireIds[$parts[0]] = true;
$clientQuestionPairs["$cc|{$parts[0]}"] = true;
}
}
}
}
if (!empty($data['client']) && !empty($data['questionnaire'])) {
foreach ($data['client'] as $c) {
if (!isset($c['clientCode'])) continue;
$cc = $c['clientCode'] ?: 'undefined';
foreach ($data['questionnaire'] as $q) {
if (!isset($q['questionnaireID'])) continue;
$qid = $q['questionnaireID'] ?: 'undefined';
$clientQuestionPairs["$cc|$qid"] = true;
}
}
}
// --- Deletions (dependents first) ---
$delAnswersByPairStmt = $pdo->prepare(
"DELETE FROM client_answer
WHERE clientCode = :cc
AND questionID IN (SELECT questionID FROM question WHERE questionnaireID = :qid)"
);
$delCompletedByPairStmt = $pdo->prepare(
"DELETE FROM completed_questionnaire
WHERE clientCode = :cc AND questionnaireID = :qid"
);
foreach ($clientQuestionPairs as $pair => $_) {
list($clientCode, $qid) = explode('|', $pair, 2);
[$cc, $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'");
$delAnswersByPairStmt->execute([':cc' => $cc, ':qid' => $qid]);
$delCompletedByPairStmt->execute([':cc' => $cc, ':qid' => $qid]);
log_msg("Deleted answers & completed for client='$cc' 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");
$delAOStmt = $pdo->prepare(
"DELETE FROM answer_option WHERE questionID IN (SELECT questionID FROM question WHERE questionnaireID = :qid)"
);
$delQTransStmt = $pdo->prepare(
"DELETE FROM question_translation WHERE questionID IN (SELECT questionID FROM question WHERE questionnaireID = :qid)"
);
$delQuestionsStmt = $pdo->prepare("DELETE FROM question WHERE questionnaireID = :qid");
foreach ($questionnaireIds as $qid => $_) {
try {
$delQuestionsStmt->execute([':questionnaireId' => $qid]);
log_msg("Deleted questions for questionnaireId='$qid'");
$delAOStmt->execute([':qid' => $qid]);
$delQTransStmt->execute([':qid' => $qid]);
$delQuestionsStmt->execute([':qid' => $qid]);
log_msg("Deleted questions/options/translations for questionnaireID='$qid'");
} catch (Exception $e) {
log_msg("Delete questions error for $qid: " . $e->getMessage());
}
@ -343,124 +197,160 @@ try {
}
// --- 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['client'])) {
$stmt = $pdo->prepare(
"INSERT OR REPLACE INTO client (clientCode, coachID) VALUES (:cc, :cid)"
);
foreach ($data['client'] as $c) {
if (!isset($c['clientCode'])) continue;
$cc = $c['clientCode'] ?: 'undefined';
$cid = $c['coachID'] ?? $entityID;
$stmt->execute([':cc' => $cc, ':cid' => $cid]);
}
}
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'] ?? '';
if (!empty($data['questionnaire'])) {
$stmt = $pdo->prepare(
"INSERT OR REPLACE INTO questionnaire (questionnaireID, name, version, state)
VALUES (:qid, :name, :ver, :state)"
);
foreach ($data['questionnaire'] as $q) {
if (!isset($q['questionnaireID'])) continue;
$stmt->execute([
':questionId' => $questionId,
':questionnaireId' => $questionnaireId,
':question' => $questionText
':qid' => $q['questionnaireID'] ?: 'undefined',
':name' => $q['name'] ?? '',
':ver' => $q['version'] ?? '',
':state' => $q['state'] ?? '',
]);
}
}
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'] : '';
if (!empty($data['question'])) {
$stmt = $pdo->prepare(
"INSERT OR IGNORE INTO question (questionID, questionnaireID, defaultText, type, orderIndex, isRequired)
VALUES (:qid, :qqid, :dt, :t, :oi, :ir)"
);
foreach ($data['question'] as $q) {
if (!isset($q['questionID'])) continue;
$stmt->execute([
':clientCode' => $clientCode,
':questionId' => $questionId,
':answerValue' => $answerValue
':qid' => $q['questionID'] ?: 'undefined',
':qqid' => $q['questionnaireID'] ?? 'undefined',
':dt' => $q['defaultText'] ?? '',
':t' => $q['type'] ?? '',
':oi' => (int)($q['orderIndex'] ?? 0),
':ir' => (int)($q['isRequired'] ?? 0),
]);
}
}
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;
if (!empty($data['answer_option'])) {
$stmt = $pdo->prepare(
"INSERT OR IGNORE INTO answer_option (answerOptionID, questionID, defaultText, points, orderIndex)
VALUES (:aoid, :qid, :dt, :pts, :oi)"
);
foreach ($data['answer_option'] as $ao) {
if (!isset($ao['answerOptionID'])) continue;
$stmt->execute([
':clientCode' => $clientCode,
':questionnaireId' => $questionnaireId,
':timestamp' => $timestamp,
':isDone' => $isDone,
':sumPoints' => $sumPoints
':aoid' => $ao['answerOptionID'],
':qid' => $ao['questionID'] ?? 'undefined',
':dt' => $ao['defaultText'] ?? '',
':pts' => (int)($ao['points'] ?? 0),
':oi' => (int)($ao['orderIndex'] ?? 0),
]);
}
}
$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)");
if (!empty($data['client_answer'])) {
$stmt = $pdo->prepare(
"INSERT OR REPLACE INTO client_answer (clientCode, questionID, answerOptionID, freeTextValue, numericValue, answeredAt)
VALUES (:cc, :qid, :aoid, :ftv, :nv, :at)"
);
foreach ($data['client_answer'] as $a) {
if (!isset($a['clientCode'], $a['questionID'])) continue;
$stmt->execute([
':cc' => $a['clientCode'] ?: 'undefined',
':qid' => $a['questionID'] ?: 'undefined',
':aoid' => $a['answerOptionID'] ?? null,
':ftv' => $a['freeTextValue'] ?? null,
':nv' => isset($a['numericValue']) ? (float)$a['numericValue'] : null,
':at' => isset($a['answeredAt']) ? (int)$a['answeredAt'] : null,
]);
}
}
@chmod($dbPath, 0644);
// Cleanup
if (!empty($data['completed_questionnaire'])) {
$stmt = $pdo->prepare(
"INSERT OR REPLACE INTO completed_questionnaire
(clientCode, questionnaireID, assignedByCoach, status, startedAt, completedAt, sumPoints)
VALUES (:cc, :qid, :abc, :st, :sa, :ca, :sp)"
);
foreach ($data['completed_questionnaire'] as $c) {
if (!isset($c['clientCode'], $c['questionnaireID'])) continue;
$stmt->execute([
':cc' => $c['clientCode'] ?: 'undefined',
':qid' => $c['questionnaireID'] ?: 'undefined',
':abc' => $c['assignedByCoach'] ?? null,
':st' => $c['status'] ?? '',
':sa' => isset($c['startedAt']) ? (int)$c['startedAt'] : null,
':ca' => isset($c['completedAt']) ? (int)$c['completedAt'] : null,
':sp' => isset($c['sumPoints']) ? (int)$c['sumPoints'] : null,
]);
}
}
if (!empty($data['question_translation'])) {
$stmt = $pdo->prepare(
"INSERT OR REPLACE INTO question_translation (questionID, languageCode, text)
VALUES (:qid, :lc, :txt)"
);
foreach ($data['question_translation'] as $qt) {
if (!isset($qt['questionID'], $qt['languageCode'])) continue;
$stmt->execute([
':qid' => $qt['questionID'],
':lc' => $qt['languageCode'],
':txt' => $qt['text'] ?? '',
]);
}
}
if (!empty($data['answer_option_translation'])) {
$stmt = $pdo->prepare(
"INSERT OR REPLACE INTO answer_option_translation (answerOptionID, languageCode, text)
VALUES (:aoid, :lc, :txt)"
);
foreach ($data['answer_option_translation'] as $aot) {
if (!isset($aot['answerOptionID'], $aot['languageCode'])) continue;
$stmt->execute([
':aoid' => $aot['answerOptionID'],
':lc' => $aot['languageCode'],
':txt' => $aot['text'] ?? '',
]);
}
}
$pdo->commit();
$pdo = null;
qdb_save($tmpDb, $lockFp);
@unlink($uploadedFilePath);
@unlink($tmpDb);
flock($lockFp, LOCK_UN);
fclose($lockFp);
echo json_encode(["success" => true, "message" => "Delta erfolgreich eingespielt und DB gespeichert"]);
echo json_encode(["success" => true, "message" => "Delta applied and DB saved"]);
exit;
} catch (Throwable $inner) {
// Cleanup bei Fehler
@flock($lockFp, LOCK_UN);
@fclose($lockFp);
@unlink($tmpDb ?? '');
@unlink($tmpEncrypted ?? '');
qdb_discard($tmpDb, $lockFp);
@unlink($uploadedFilePath ?? '');
log_msg("Inner exception: " . $inner->getMessage());
http_response_code(500);
echo json_encode(["error" => "Fehler beim Speichern", "message" => $inner->getMessage()]);
error_log($inner->getMessage());
echo json_encode(["error" => "Save error"]);
exit;
}
} catch (Throwable $e) {
log_msg("Top-level exception: " . $e->getMessage());
http_response_code(500);
echo json_encode(["error" => "Fehler", "message" => $e->getMessage()]);
error_log($e->getMessage());
echo json_encode(["error" => "Server error"]);
exit;
}