Files
nat-as-server/uploadDeltaTest5.php
2026-03-24 10:22:01 +00:00

467 lines
20 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
// /var/www/html/uploadDeltaTest5.php
// Vollständige, eigenständige Version einfach kopieren & einfügen.
// -----------------------------
// Logging & PHP-Settings
// -----------------------------
error_reporting(E_ALL);
ini_set('display_errors', '0');
ini_set('log_errors', '1');
$LOG_FILE = __DIR__ . '/uploadDelta.log';
function log_msg($msg) {
global $LOG_FILE;
error_log(date('[Y-m-d H:i:s] ') . $msg . PHP_EOL, 3, $LOG_FILE);
}
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"]);
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;
}