357 lines
14 KiB
PHP
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;
|
|
}
|