Files
nat-as-server/uploadDeltaTest5.php
2026-04-15 10:19:42 +02:00

357 lines
14 KiB
PHP

<?php
// /var/www/html/uploadDeltaTest5.php
require_once __DIR__ . '/db_init.php';
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');
try {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(["error" => "Only POST allowed"]);
exit;
}
// Token from multipart field or Authorization: Bearer
$token = $_POST['token'] ?? null;
if (!$token) {
$auth = $_SERVER['HTTP_AUTHORIZATION'] ?? ($_SERVER['Authorization'] ?? '');
if (stripos($auth, 'Bearer ') === 0) {
$token = substr($auth, 7);
}
}
$tokenRec = $token ? token_get_record($token) : null;
if (!$tokenRec || !empty($tokenRec['temp'])) {
http_response_code(403);
echo json_encode(["error" => "Invalid token"]);
exit;
}
// Upload: multipart 'file' or 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" => "No file sent"]);
exit;
}
// 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" => "Failed to read uploaded file"]);
exit;
}
$sessionKey = hkdf_session_key_from_token($token);
$jsonData = null;
try {
$jsonData = aes256_cbc_decrypt_bytes($encData, $sessionKey);
} catch (Throwable $e) {
$maybeJson = trim($encData);
if ($maybeJson === '' || json_decode($maybeJson, true) === null) {
log_msg("Decryption failed and not valid JSON. raw len=" . strlen($encData));
http_response_code(400);
echo json_encode(["error" => "Invalid encrypted file or not JSON"]);
exit;
}
$jsonData = $maybeJson;
}
$data = json_decode($jsonData, true);
if (!is_array($data)) {
http_response_code(400);
echo json_encode(["error" => "Invalid JSON"]);
exit;
}
// RBAC: coaches can only upload for their own clients
$role = $tokenRec['role'] ?? '';
$entityID = $tokenRec['entityID'] ?? '';
[$pdo, $tmpDb, $lockFp] = qdb_open(true);
try {
$pdo->beginTransaction();
if ($role === 'coach' && !empty($data['client'])) {
foreach ($data['client'] as $c) {
if (!isset($c['clientCode'])) continue;
$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;
}
}
}
// --- 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 => $_) {
[$cc, $qid] = explode('|', $pair, 2);
try {
$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)) {
$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 {
$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());
}
}
}
// --- Inserts/Upserts ---
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['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([
':qid' => $q['questionnaireID'] ?: 'undefined',
':name' => $q['name'] ?? '',
':ver' => $q['version'] ?? '',
':state' => $q['state'] ?? '',
]);
}
}
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([
':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['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([
':aoid' => $ao['answerOptionID'],
':qid' => $ao['questionID'] ?? 'undefined',
':dt' => $ao['defaultText'] ?? '',
':pts' => (int)($ao['points'] ?? 0),
':oi' => (int)($ao['orderIndex'] ?? 0),
]);
}
}
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,
]);
}
}
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);
echo json_encode(["success" => true, "message" => "Delta applied and DB saved"]);
exit;
} catch (Throwable $inner) {
qdb_discard($tmpDb, $lockFp);
@unlink($uploadedFilePath ?? '');
log_msg("Inner exception: " . $inner->getMessage());
http_response_code(500);
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);
error_log($e->getMessage());
echo json_encode(["error" => "Server error"]);
exit;
}