From fc84d55bd620ab0f728d60b6d2483bb21496aabe Mon Sep 17 00:00:00 2001 From: Tom Hempel Date: Wed, 3 Jun 2026 17:27:51 +0200 Subject: [PATCH] further enhanced error handling + added retries in case of parallel writes --- .gitignore | 2 +- api/index.php | 6 +-- db_init.php | 59 ++++++++++++--------- handlers/analytics.php | 20 +------ handlers/answer_options.php | 37 ++++--------- handlers/app_questionnaires.php | 13 +---- handlers/assignments.php | 13 ++--- handlers/auth.php | 12 ++--- handlers/clients.php | 19 +++---- handlers/coaches.php | 10 +--- handlers/dev.php | 21 +++----- handlers/export.php | 30 ++--------- handlers/logout.php | 7 +-- handlers/questionnaires.php | 60 ++++++--------------- handlers/questions.php | 41 +++------------ handlers/results.php | 10 +--- handlers/session.php | 10 +--- handlers/translations.php | 93 +++++---------------------------- handlers/users.php | 36 +++++-------- lib/errors.php | 62 +++++++++++++++++++++- website/js/api.js | 6 --- 21 files changed, 184 insertions(+), 383 deletions(-) diff --git a/.gitignore b/.gitignore index b4d7a5e..c3a775f 100644 --- a/.gitignore +++ b/.gitignore @@ -49,7 +49,7 @@ Thumbs.db # Misc *.orig *.rej - +*.md # Ignore browser build artifacts (if any) website/js/dist/ diff --git a/api/index.php b/api/index.php index 62029d3..9a6a554 100644 --- a/api/index.php +++ b/api/index.php @@ -6,6 +6,7 @@ require_once __DIR__ . '/../common.php'; require_once __DIR__ . '/../db_init.php'; require_once __DIR__ . '/../lib/response.php'; +require_once __DIR__ . '/../lib/errors.php'; require_once __DIR__ . '/../lib/validate.php'; require_once __DIR__ . '/../lib/api_log.php'; @@ -71,7 +72,6 @@ try { require $handlerFile; } catch (Throwable $e) { - qdb_api_log_note_exception($e); - error_log("Unhandled error on $method $route: " . $e->getMessage()); - json_error('SERVER_ERROR', 'Internal server error', 500); + require_once __DIR__ . '/../lib/errors.php'; + qdb_emit_api_error($e, "API $method $route"); } diff --git a/db_init.php b/db_init.php index 7730ab9..5fdd831 100644 --- a/db_init.php +++ b/db_init.php @@ -140,18 +140,37 @@ function qdb_apply_migrations(PDO $pdo, string $schemaSql): bool { return $changed; } -function qdb_acquire_lock() { +/** + * Exclusive lock with short retries (non-blocking attempts). Avoids hung PHP workers + * when two writes overlap (app upload + website edit). + */ +function qdb_flock_exclusive_with_retry($lockFp, int $maxAttempts = 12, int $sleepMicros = 75000): void +{ + for ($attempt = 0; $attempt < $maxAttempts; $attempt++) { + if (flock($lockFp, LOCK_EX | LOCK_NB)) { + return; + } + if ($attempt < $maxAttempts - 1) { + usleep($sleepMicros); + } + } + throw new Exception('Could not acquire lock'); +} + +function qdb_open_writable_lock() +{ $lockFp = fopen(QDB_LOCK, 'c'); if ($lockFp === false) { throw new Exception('Could not open lock file'); } - if (!flock($lockFp, LOCK_EX)) { - fclose($lockFp); - throw new Exception('Could not acquire lock'); - } + qdb_flock_exclusive_with_retry($lockFp); return $lockFp; } +function qdb_acquire_lock() { + return qdb_open_writable_lock(); +} + /** * Decrypt the master DB file into a temp SQLite file, or create a fresh one * from schema.sql if no DB exists yet. @@ -171,12 +190,7 @@ function qdb_open(bool $writable = false): array { $lockFp = null; if ($writable) { - $lockFp = fopen(QDB_LOCK, 'c'); - if ($lockFp === false) throw new Exception("Could not open lock file"); - if (!flock($lockFp, LOCK_EX)) { - fclose($lockFp); - throw new Exception("Could not acquire lock"); - } + $lockFp = qdb_open_writable_lock(); } $tmpDb = tempnam(sys_get_temp_dir(), 'qdb_'); @@ -289,33 +303,26 @@ function qdb_discard(string $tmpDb, $lockFp): void { * * @return array{0: PDO, 1: string, 2: resource|null}|null */ -function qdb_open_read_or_fail(): ?array { +/** + * @return array{0: PDO, 1: string, 2: resource|null} + */ +function qdb_open_read_or_fail(): array { try { return qdb_open(false); } catch (Throwable $e) { - if (function_exists('qdb_api_log_note_exception')) { - qdb_api_log_note_exception($e); - } require_once __DIR__ . '/lib/errors.php'; - error_log('qdb_open_read: ' . $e->getMessage()); - json_error('SERVER_ERROR', qdb_public_error_message($e, 'Database read'), 500); + qdb_emit_api_error($e, 'Database read'); } } /** - * Open a writable DB connection; on failure logs and returns a JSON error response. - * - * @return array{0: PDO, 1: string, 2: resource|null}|null + * @return array{0: PDO, 1: string, 2: resource|null} */ -function qdb_open_write_or_fail(): ?array { +function qdb_open_write_or_fail(): array { try { return qdb_open(true); } catch (Throwable $e) { - if (function_exists('qdb_api_log_note_exception')) { - qdb_api_log_note_exception($e); - } require_once __DIR__ . '/lib/errors.php'; - error_log('qdb_open_write: ' . $e->getMessage()); - json_error('SERVER_ERROR', qdb_public_error_message($e, 'Database write'), 500); + qdb_emit_api_error($e, 'Database write'); } } diff --git a/handlers/analytics.php b/handlers/analytics.php index 267290d..5c6e205 100644 --- a/handlers/analytics.php +++ b/handlers/analytics.php @@ -26,15 +26,7 @@ case 'GET': qdb_discard($tmpDb, $lockFp); json_error('BAD_REQUEST', 'Unknown query', 400); } catch (Throwable $e) { - if (isset($tmpDb, $lockFp)) { - qdb_discard($tmpDb, $lockFp); - } - if (function_exists('qdb_api_log_note_exception')) { - qdb_api_log_note_exception($e); - } - require_once __DIR__ . '/../lib/errors.php'; - error_log('analytics GET: ' . $e->getMessage()); - json_error('SERVER_ERROR', qdb_public_error_message($e, 'Analytics'), 500); + qdb_handler_fail($e, 'Analytics', null, $tmpDb ?? null, $lockFp ?? null); } break; @@ -50,15 +42,7 @@ case 'PUT': qdb_save($tmpDb, $lockFp); json_success(['clientCode' => $clientCode, 'note' => $note]); } catch (Throwable $e) { - if (isset($tmpDb, $lockFp)) { - qdb_discard($tmpDb, $lockFp); - } - if (function_exists('qdb_api_log_note_exception')) { - qdb_api_log_note_exception($e); - } - require_once __DIR__ . '/../lib/errors.php'; - error_log('analytics PUT: ' . $e->getMessage()); - json_error('SERVER_ERROR', qdb_public_error_message($e, 'Save note'), 500); + qdb_handler_fail($e, 'Save follow-up note', null, $tmpDb ?? null, $lockFp ?? null); } break; diff --git a/handlers/answer_options.php b/handlers/answer_options.php index 8befcdf..fe8e162 100644 --- a/handlers/answer_options.php +++ b/handlers/answer_options.php @@ -29,15 +29,7 @@ case 'GET': qdb_discard($tmpDb, $lockFp); json_success(['answerOptions' => $options]); } catch (Throwable $e) { - if (isset($tmpDb, $lockFp)) { - qdb_discard($tmpDb, $lockFp); - } - if (function_exists('qdb_api_log_note_exception')) { - qdb_api_log_note_exception($e); - } - require_once __DIR__ . '/../lib/errors.php'; - error_log('answer_options GET: ' . $e->getMessage()); - json_error('SERVER_ERROR', qdb_public_error_message($e, 'Load answer options'), 500); + qdb_handler_fail($e, 'Load answer options', null, $tmpDb ?? null, $lockFp ?? null); } break; @@ -56,7 +48,7 @@ case 'POST': $order = (int)($body['orderIndex'] ?? 0); $nextQ = trim($body['nextQuestionId'] ?? ''); - [$pdo, $tmpDb, $lockFp] = qdb_open(true); + [$pdo, $tmpDb, $lockFp] = qdb_open_write_or_fail(); try { $chk = $pdo->prepare('SELECT 1 FROM question WHERE questionID = :id'); $chk->execute([':id' => $qID]); @@ -86,9 +78,7 @@ case 'POST': 'translations' => [], ]]); } catch (Throwable $e) { - qdb_discard($tmpDb, $lockFp); - error_log($e->getMessage()); - json_error('SERVER_ERROR', 'Server error', 500); + qdb_handler_fail($e, 'Answer options', null, $tmpDb ?? null, $lockFp ?? null); } break; @@ -100,7 +90,7 @@ case 'PUT': } $id = $body['answerOptionID']; - [$pdo, $tmpDb, $lockFp] = qdb_open(true); + [$pdo, $tmpDb, $lockFp] = qdb_open_write_or_fail(); try { $existing = $pdo->prepare('SELECT * FROM answer_option WHERE answerOptionID = :id'); $existing->execute([':id' => $id]); @@ -140,9 +130,7 @@ case 'PUT': 'nextQuestionId' => $nextQ, ]]); } catch (Throwable $e) { - qdb_discard($tmpDb, $lockFp); - error_log($e->getMessage()); - json_error('SERVER_ERROR', 'Server error', 500); + qdb_handler_fail($e, 'Answer options', null, $tmpDb ?? null, $lockFp ?? null); } break; @@ -154,7 +142,7 @@ case 'DELETE': } $id = $body['answerOptionID']; - [$pdo, $tmpDb, $lockFp] = qdb_open(true); + [$pdo, $tmpDb, $lockFp] = qdb_open_write_or_fail(); try { $pdo->exec('PRAGMA foreign_keys = OFF;'); $pdo->beginTransaction(); @@ -166,12 +154,7 @@ case 'DELETE': qdb_save($tmpDb, $lockFp); json_success(['deleted' => true]); } catch (Throwable $e) { - if ($pdo->inTransaction()) { - $pdo->rollBack(); - } - qdb_discard($tmpDb, $lockFp); - error_log($e->getMessage()); - json_error('SERVER_ERROR', 'Server error', 500); + qdb_handler_fail($e, 'Delete answer option', $pdo ?? null, $tmpDb ?? null, $lockFp ?? null); } break; @@ -181,7 +164,7 @@ case 'PATCH': if (empty($body['questionID']) || !is_array($body['order'] ?? null)) { json_error('MISSING_FIELDS', 'questionID and order[] required', 400); } - [$pdo, $tmpDb, $lockFp] = qdb_open(true); + [$pdo, $tmpDb, $lockFp] = qdb_open_write_or_fail(); try { $stmt = $pdo->prepare('UPDATE answer_option SET orderIndex = :o WHERE answerOptionID = :id AND questionID = :qid'); foreach ($body['order'] as $idx => $aoid) { @@ -190,9 +173,7 @@ case 'PATCH': qdb_save($tmpDb, $lockFp); json_success(['reordered' => true]); } catch (Throwable $e) { - qdb_discard($tmpDb, $lockFp); - error_log($e->getMessage()); - json_error('SERVER_ERROR', 'Server error', 500); + qdb_handler_fail($e, 'Answer options', null, $tmpDb ?? null, $lockFp ?? null); } break; diff --git a/handlers/app_questionnaires.php b/handlers/app_questionnaires.php index dac8c02..381b68a 100644 --- a/handlers/app_questionnaires.php +++ b/handlers/app_questionnaires.php @@ -254,18 +254,7 @@ if ($method === 'POST') { json_success(['submitted' => true, 'sumPoints' => $sumPoints]); } catch (Throwable $e) { - if (isset($pdo) && $pdo->inTransaction()) { - $pdo->rollBack(); - } - if (isset($tmpDb, $lockFp)) { - qdb_discard($tmpDb, $lockFp); - } - if (function_exists('qdb_api_log_note_exception')) { - qdb_api_log_note_exception($e); - } - require_once __DIR__ . '/../lib/errors.php'; - error_log('app_questionnaires POST: ' . $e->getMessage()); - json_error('SERVER_ERROR', qdb_public_error_message($e, 'Submit'), 500); + qdb_handler_fail($e, 'Submit questionnaire', $pdo ?? null, $tmpDb ?? null, $lockFp ?? null); } } diff --git a/handlers/assignments.php b/handlers/assignments.php index 4846356..cd1275d 100644 --- a/handlers/assignments.php +++ b/handlers/assignments.php @@ -9,7 +9,7 @@ switch ($method) { case 'GET': try { - [$pdo, $tmpDb, $lockFp] = qdb_open(false); + [$pdo, $tmpDb, $lockFp] = qdb_open_read_or_fail(); if ($callerRole === 'admin') { $coaches = $pdo->query( @@ -44,9 +44,7 @@ case 'GET': qdb_discard($tmpDb, $lockFp); json_success(['coaches' => $coaches, 'clients' => $clients]); } catch (Throwable $e) { - if (isset($tmpDb, $lockFp)) qdb_discard($tmpDb, $lockFp); - error_log($e->getMessage()); - json_error('SERVER_ERROR', 'Server error', 500); + qdb_handler_fail($e, 'assignments', null, $tmpDb ?? null, $lockFp ?? null); } break; @@ -61,7 +59,7 @@ case 'POST': if (!is_array($clientCodes)) $clientCodes = [$clientCodes]; try { - [$pdo, $tmpDb, $lockFp] = qdb_open(true); + [$pdo, $tmpDb, $lockFp] = qdb_open_write_or_fail(); if ($callerRole === 'supervisor') { $chk = $pdo->prepare("SELECT coachID FROM coach WHERE coachID = :cid AND supervisorID = :sid"); @@ -92,10 +90,7 @@ case 'POST': json_success(['assigned' => $count]); } catch (Throwable $e) { - if (isset($pdo) && $pdo->inTransaction()) $pdo->rollBack(); - if (isset($tmpDb, $lockFp)) qdb_discard($tmpDb, $lockFp); - error_log($e->getMessage()); - json_error('SERVER_ERROR', 'Server error', 500); + qdb_handler_fail($e, 'assignments', $pdo ?? null, $tmpDb ?? null, $lockFp ?? null); } break; diff --git a/handlers/auth.php b/handlers/auth.php index c8cc92d..431f009 100644 --- a/handlers/auth.php +++ b/handlers/auth.php @@ -16,7 +16,7 @@ case 'auth/login': } try { - [$pdo, $tmpDb, $lockFp] = qdb_open(false); + [$pdo, $tmpDb, $lockFp] = qdb_open_read_or_fail(); $stmt = $pdo->prepare( "SELECT userID, passwordHash, role, entityID, mustChangePassword FROM users WHERE username = :u" @@ -80,9 +80,7 @@ case 'auth/login': } json_success($loginData); } catch (Throwable $e) { - if (isset($tmpDb, $lockFp)) qdb_discard($tmpDb, $lockFp); - error_log($e->getMessage()); - json_error('SERVER_ERROR', 'Server error', 500); + qdb_handler_fail($e, 'auth', null, $tmpDb ?? null, $lockFp ?? null); } break; @@ -117,7 +115,7 @@ case 'auth/change-password': } try { - [$pdo, $tmpDb, $lockFp] = qdb_open(true); + [$pdo, $tmpDb, $lockFp] = qdb_open_write_or_fail(); $stmt = $pdo->prepare( "SELECT userID, passwordHash, role, entityID FROM users WHERE username = :u" @@ -165,9 +163,7 @@ case 'auth/change-password': 'role' => $user['role'], ]); } catch (Throwable $e) { - if (isset($tmpDb, $lockFp)) qdb_discard($tmpDb, $lockFp); - error_log($e->getMessage()); - json_error('SERVER_ERROR', 'Server error', 500); + qdb_handler_fail($e, 'auth', null, $tmpDb ?? null, $lockFp ?? null); } break; diff --git a/handlers/clients.php b/handlers/clients.php index 96dfc07..4bbf3a6 100644 --- a/handlers/clients.php +++ b/handlers/clients.php @@ -9,7 +9,7 @@ switch ($method) { case 'GET': try { - [$pdo, $tmpDb, $lockFp] = qdb_open(false); + [$pdo, $tmpDb, $lockFp] = qdb_open_read_or_fail(); $clientCode = trim((string)($_GET['clientCode'] ?? '')); if ($clientCode !== '' && !empty($_GET['detail'])) { @@ -39,9 +39,7 @@ case 'GET': qdb_discard($tmpDb, $lockFp); json_success(['clients' => $clients]); } catch (Throwable $e) { - if (isset($tmpDb, $lockFp)) qdb_discard($tmpDb, $lockFp); - error_log($e->getMessage()); - json_error('SERVER_ERROR', 'Server error', 500); + qdb_handler_fail($e, 'clients', null, $tmpDb ?? null, $lockFp ?? null); } break; @@ -55,7 +53,7 @@ case 'POST': } try { - [$pdo, $tmpDb, $lockFp] = qdb_open(true); + [$pdo, $tmpDb, $lockFp] = qdb_open_write_or_fail(); // Validate coach exists and caller is allowed to assign to them if ($callerRole === 'supervisor') { @@ -83,9 +81,7 @@ case 'POST': qdb_save($tmpDb, $lockFp); json_success(['clientCode' => $clientCode, 'coachID' => $coachID]); } catch (Throwable $e) { - if (isset($tmpDb, $lockFp)) qdb_discard($tmpDb, $lockFp); - error_log($e->getMessage()); - json_error('SERVER_ERROR', 'Server error', 500); + qdb_handler_fail($e, 'clients', null, $tmpDb ?? null, $lockFp ?? null); } break; @@ -98,7 +94,7 @@ case 'DELETE': } try { - [$pdo, $tmpDb, $lockFp] = qdb_open(true); + [$pdo, $tmpDb, $lockFp] = qdb_open_write_or_fail(); // Verify the client exists and the caller can see it (RBAC) [$clause, $params] = rbac_client_filter($tokenRec, 'cl'); @@ -122,10 +118,7 @@ case 'DELETE': qdb_save($tmpDb, $lockFp); json_success([]); } catch (Throwable $e) { - if (isset($pdo) && $pdo->inTransaction()) $pdo->rollBack(); - if (isset($tmpDb, $lockFp)) qdb_discard($tmpDb, $lockFp); - error_log($e->getMessage()); - json_error('SERVER_ERROR', 'Server error', 500); + qdb_handler_fail($e, 'clients', $pdo ?? null, $tmpDb ?? null, $lockFp ?? null); } break; diff --git a/handlers/coaches.php b/handlers/coaches.php index 3f703e9..9f593f1 100644 --- a/handlers/coaches.php +++ b/handlers/coaches.php @@ -25,13 +25,5 @@ try { qdb_discard($tmpDb, $lockFp); json_success(['coaches' => $coaches]); } catch (Throwable $e) { - if (isset($tmpDb, $lockFp)) { - qdb_discard($tmpDb, $lockFp); - } - if (function_exists('qdb_api_log_note_exception')) { - qdb_api_log_note_exception($e); - } - require_once __DIR__ . '/../lib/errors.php'; - error_log('coaches GET: ' . $e->getMessage()); - json_error('SERVER_ERROR', qdb_public_error_message($e, 'Load coaches'), 500); + qdb_handler_fail($e, 'Load coaches', null, $tmpDb ?? null, $lockFp ?? null); } diff --git a/handlers/dev.php b/handlers/dev.php index dbf8ddd..f544713 100644 --- a/handlers/dev.php +++ b/handlers/dev.php @@ -16,7 +16,7 @@ $body = read_json_body(); $action = trim((string)($body['action'] ?? 'import')); try { - [$pdo, $tmpDb, $lockFp] = qdb_open(true); + [$pdo, $tmpDb, $lockFp] = qdb_open_write_or_fail(); if ($action === 'remove') { $result = qdb_remove_dev_test_data($pdo, [ @@ -43,20 +43,11 @@ try { qdb_save($tmpDb, $lockFp); json_success($result); } catch (Throwable $e) { - if (isset($tmpDb, $lockFp)) { - qdb_discard($tmpDb, $lockFp); - } $labels = [ - 'remove' => 'Remove', - 'wipeExceptAdmins' => 'Wipe', - 'import' => 'Import', + 'remove' => 'Remove dev data', + 'wipeExceptAdmins' => 'Wipe database', + 'import' => 'Import dev fixture', ]; - $label = $labels[$action ?? 'import'] ?? 'Operation'; - error_log("dev fixture $label: " . $e->getMessage()); - require_once __DIR__ . '/../lib/errors.php'; - if (function_exists('qdb_api_log_note_exception')) { - qdb_api_log_note_exception($e); - } - error_log("dev fixture $label: " . $e->getMessage()); - json_error('SERVER_ERROR', qdb_public_error_message($e, $label), 500); + $label = $labels[$action ?? 'import'] ?? 'Dev fixture'; + qdb_handler_fail($e, $label, $pdo ?? null, $tmpDb ?? null, $lockFp ?? null); } diff --git a/handlers/export.php b/handlers/export.php index 2c0f749..ae01601 100644 --- a/handlers/export.php +++ b/handlers/export.php @@ -22,15 +22,7 @@ if (!empty($_GET['bundle'])) { echo json_encode($bundle, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); exit; } catch (Throwable $e) { - if (isset($tmpDb, $lockFp)) { - qdb_discard($tmpDb, $lockFp); - } - if (function_exists('qdb_api_log_note_exception')) { - qdb_api_log_note_exception($e); - } - require_once __DIR__ . '/../lib/errors.php'; - error_log('export bundle: ' . $e->getMessage()); - json_error('SERVER_ERROR', qdb_public_error_message($e, 'Export'), 500); + qdb_handler_fail($e, 'Export questionnaires bundle', null, $tmpDb ?? null, $lockFp ?? null); } } @@ -53,15 +45,7 @@ if (!empty($_GET['exportAll'])) { @unlink($zipPath); exit; } catch (Throwable $e) { - if (isset($tmpDb, $lockFp)) { - qdb_discard($tmpDb, $lockFp); - } - if (function_exists('qdb_api_log_note_exception')) { - qdb_api_log_note_exception($e); - } - require_once __DIR__ . '/../lib/errors.php'; - error_log('export exportAll: ' . $e->getMessage()); - json_error('SERVER_ERROR', qdb_public_error_message($e, 'Export'), 500); + qdb_handler_fail($e, 'Export responses ZIP', null, $tmpDb ?? null, $lockFp ?? null); } } @@ -112,13 +96,5 @@ $filename = $safeName . '_v' . $questionnaire['version'] . '.csv'; header('Content-Disposition: attachment; filename="' . $filename . '"'); echo qdb_export_rows_to_csv_string($rows, qdb_export_current_csv_fallback_header($resultColumns)); } catch (Throwable $e) { - if (isset($tmpDb, $lockFp)) { - qdb_discard($tmpDb, $lockFp); - } - if (function_exists('qdb_api_log_note_exception')) { - qdb_api_log_note_exception($e); - } - require_once __DIR__ . '/../lib/errors.php'; - error_log('export csv: ' . $e->getMessage()); - json_error('SERVER_ERROR', qdb_public_error_message($e, 'Export'), 500); + qdb_handler_fail($e, 'Export CSV', null, $tmpDb ?? null, $lockFp ?? null); } diff --git a/handlers/logout.php b/handlers/logout.php index 3990214..3dd0aa6 100644 --- a/handlers/logout.php +++ b/handlers/logout.php @@ -12,12 +12,7 @@ if (!$token) { try { token_revoke($token); } catch (Throwable $e) { - if (function_exists('qdb_api_log_note_exception')) { - qdb_api_log_note_exception($e); - } - require_once __DIR__ . '/../lib/errors.php'; - error_log('logout: ' . $e->getMessage()); - json_error('SERVER_ERROR', qdb_public_error_message($e, 'Logout'), 500); + qdb_handler_fail($e, 'Logout'); } json_success(['loggedOut' => true]); diff --git a/handlers/questionnaires.php b/handlers/questionnaires.php index 6d718ad..3a172a9 100644 --- a/handlers/questionnaires.php +++ b/handlers/questionnaires.php @@ -40,15 +40,7 @@ case 'GET': qdb_discard($tmpDb, $lockFp); json_success(['questionnaires' => $rows, 'categoryKeys' => $categoryKeys]); } catch (Throwable $e) { - if (isset($tmpDb, $lockFp)) { - qdb_discard($tmpDb, $lockFp); - } - if (function_exists('qdb_api_log_note_exception')) { - qdb_api_log_note_exception($e); - } - require_once __DIR__ . '/../lib/errors.php'; - error_log('questionnaires GET: ' . $e->getMessage()); - json_error('SERVER_ERROR', qdb_public_error_message($e, 'Load questionnaires'), 500); + qdb_handler_fail($e, 'Load questionnaires', null, $tmpDb ?? null, $lockFp ?? null); } break; @@ -59,7 +51,7 @@ case 'POST': if (($body['action'] ?? '') === 'updateConditionMessageLabel') { $messageKey = trim((string)($body['messageKey'] ?? '')); $germanLabel = (string)($body['germanLabel'] ?? ''); - [$pdo, $tmpDb, $lockFp] = qdb_open(true); + [$pdo, $tmpDb, $lockFp] = qdb_open_write_or_fail(); try { qdb_set_app_string_german_label($pdo, $messageKey, $germanLabel); qdb_save($tmpDb, $lockFp); @@ -68,9 +60,7 @@ case 'POST': 'germanLabel' => trim($germanLabel), ]); } catch (Throwable $e) { - qdb_discard($tmpDb, $lockFp); - error_log('updateConditionMessageLabel: ' . $e->getMessage()); - json_error('SERVER_ERROR', 'Server error', 500); + qdb_handler_fail($e, 'Update condition message label', null, $tmpDb, $lockFp); } break; } @@ -78,7 +68,7 @@ case 'POST': if (($body['action'] ?? '') === 'updateCategoryLabel') { $categoryKey = trim((string)($body['categoryKey'] ?? '')); $germanLabel = (string)($body['germanLabel'] ?? ''); - [$pdo, $tmpDb, $lockFp] = qdb_open(true); + [$pdo, $tmpDb, $lockFp] = qdb_open_write_or_fail(); try { $stringKey = qdb_set_category_german_label($pdo, $categoryKey, $germanLabel); qdb_save($tmpDb, $lockFp); @@ -88,9 +78,7 @@ case 'POST': 'germanLabel' => trim($germanLabel), ]); } catch (Throwable $e) { - qdb_discard($tmpDb, $lockFp); - error_log('updateCategoryLabel: ' . $e->getMessage()); - json_error('SERVER_ERROR', 'Server error', 500); + qdb_handler_fail($e, 'Update category label', null, $tmpDb, $lockFp); } break; } @@ -100,15 +88,13 @@ case 'POST': if ($categoryKey === '') { json_error('INVALID_FIELD', 'categoryKey is required', 400); } - [$pdo, $tmpDb, $lockFp] = qdb_open(true); + [$pdo, $tmpDb, $lockFp] = qdb_open_write_or_fail(); try { $cleared = qdb_delete_category_key($pdo, $categoryKey); qdb_save($tmpDb, $lockFp); json_success(['deleted' => $categoryKey, 'questionnairesCleared' => $cleared]); } catch (Throwable $e) { - qdb_discard($tmpDb, $lockFp); - error_log('deleteCategoryKey: ' . $e->getMessage()); - json_error('SERVER_ERROR', 'Server error', 500); + qdb_handler_fail($e, 'Delete category key', null, $tmpDb, $lockFp); } break; } @@ -119,7 +105,7 @@ case 'POST': json_error('MISSING_FIELDS', 'bundle object is required', 400); } $replaceIfExists = !empty($body['replaceIfExists']); - [$pdo, $tmpDb, $lockFp] = qdb_open(true); + [$pdo, $tmpDb, $lockFp] = qdb_open_write_or_fail(); try { $pdo->exec('PRAGMA foreign_keys = OFF;'); $result = qdb_import_questionnaires_bundle($pdo, $bundle, $replaceIfExists); @@ -127,14 +113,7 @@ case 'POST': qdb_save($tmpDb, $lockFp); json_success($result); } catch (Throwable $e) { - qdb_discard($tmpDb, $lockFp); - error_log('importBundle: ' . $e->getMessage()); - require_once __DIR__ . '/../lib/errors.php'; - if (function_exists('qdb_api_log_note_exception')) { - qdb_api_log_note_exception($e); - } - error_log('importBundle: ' . $e->getMessage()); - json_error('SERVER_ERROR', qdb_public_error_message($e, 'Import'), 500); + qdb_handler_fail($e, 'Import questionnaires', null, $tmpDb, $lockFp); } break; } @@ -150,7 +129,7 @@ case 'POST': $showPts = (int)($body['showPoints'] ?? 0); $condJson = $body['conditionJson'] ?? '{}'; - [$pdo, $tmpDb, $lockFp] = qdb_open(true); + [$pdo, $tmpDb, $lockFp] = qdb_open_write_or_fail(); try { if ($order === 0) { $order = (int)$pdo->query("SELECT COALESCE(MAX(orderIndex),0)+1 FROM questionnaire")->fetchColumn(); @@ -170,9 +149,7 @@ case 'POST': 'conditionJson' => $condJson, 'questionCount' => 0, ]]); } catch (Throwable $e) { - qdb_discard($tmpDb, $lockFp); - error_log($e->getMessage()); - json_error('SERVER_ERROR', 'Server error', 500); + qdb_handler_fail($e, 'Create questionnaire', null, $tmpDb, $lockFp); } break; @@ -184,7 +161,7 @@ case 'PUT': } $id = $body['questionnaireID']; - [$pdo, $tmpDb, $lockFp] = qdb_open(true); + [$pdo, $tmpDb, $lockFp] = qdb_open_write_or_fail(); try { $existing = $pdo->prepare('SELECT * FROM questionnaire WHERE questionnaireID = :id'); $existing->execute([':id' => $id]); @@ -217,9 +194,7 @@ case 'PUT': 'orderIndex' => $order, 'showPoints' => $showPts, 'conditionJson' => $condJson, ]]); } catch (Throwable $e) { - qdb_discard($tmpDb, $lockFp); - error_log($e->getMessage()); - json_error('SERVER_ERROR', 'Server error', 500); + qdb_handler_fail($e, 'Update questionnaire', null, $tmpDb, $lockFp); } break; @@ -231,7 +206,7 @@ case 'DELETE': } $id = $body['questionnaireID']; - [$pdo, $tmpDb, $lockFp] = qdb_open(true); + [$pdo, $tmpDb, $lockFp] = qdb_open_write_or_fail(); try { $pdo->exec('PRAGMA foreign_keys = OFF;'); $pdo->beginTransaction(); @@ -260,12 +235,7 @@ case 'DELETE': qdb_save($tmpDb, $lockFp); json_success([]); } catch (Throwable $e) { - if ($pdo->inTransaction()) { - $pdo->rollBack(); - } - qdb_discard($tmpDb, $lockFp); - error_log($e->getMessage()); - json_error('SERVER_ERROR', 'Server error', 500); + qdb_handler_fail($e, 'Delete questionnaire', $pdo, $tmpDb, $lockFp); } break; diff --git a/handlers/questions.php b/handlers/questions.php index f38b7a0..91d78d1 100644 --- a/handlers/questions.php +++ b/handlers/questions.php @@ -46,15 +46,7 @@ case 'GET': qdb_discard($tmpDb, $lockFp); json_success(['questions' => $questions]); } catch (Throwable $e) { - if (isset($tmpDb, $lockFp)) { - qdb_discard($tmpDb, $lockFp); - } - if (function_exists('qdb_api_log_note_exception')) { - qdb_api_log_note_exception($e); - } - require_once __DIR__ . '/../lib/errors.php'; - error_log('questions GET: ' . $e->getMessage()); - json_error('SERVER_ERROR', qdb_public_error_message($e, 'Load questions'), 500); + qdb_handler_fail($e, 'Load questions', null, $tmpDb ?? null, $lockFp ?? null); } break; @@ -79,7 +71,7 @@ case 'POST': $body['noteAfter'] ?? ($cfg['noteAfter'] ?? null) ); $configJson = json_encode($config, JSON_UNESCAPED_UNICODE); - [$pdo, $tmpDb, $lockFp] = qdb_open(true); + [$pdo, $tmpDb, $lockFp] = qdb_open_write_or_fail(); try { $chk = $pdo->prepare("SELECT 1 FROM questionnaire WHERE questionnaireID = :id"); $chk->execute([':id' => $qnID]); @@ -117,9 +109,7 @@ case 'POST': 'configJson' => $configJson, 'answerOptions' => [], 'translations' => [], ]]); } catch (Throwable $e) { - qdb_discard($tmpDb, $lockFp); - error_log($e->getMessage()); - json_error('SERVER_ERROR', 'Server error', 500); + qdb_handler_fail($e, 'Questions', null, $tmpDb ?? null, $lockFp ?? null); } break; @@ -130,7 +120,7 @@ case 'PUT': json_error('BAD_REQUEST', 'questionID is required', 400); } $id = $body['questionID']; - [$pdo, $tmpDb, $lockFp] = qdb_open(true); + [$pdo, $tmpDb, $lockFp] = qdb_open_write_or_fail(); try { $existing = $pdo->prepare("SELECT * FROM question WHERE questionID = :id"); $existing->execute([':id' => $id]); @@ -171,9 +161,7 @@ case 'PUT': 'configJson' => $configJson, ]]); } catch (Throwable $e) { - qdb_discard($tmpDb, $lockFp); - error_log($e->getMessage()); - json_error('SERVER_ERROR', 'Server error', 500); + qdb_handler_fail($e, 'Questions', null, $tmpDb ?? null, $lockFp ?? null); } break; @@ -184,7 +172,7 @@ case 'DELETE': json_error('BAD_REQUEST', 'questionID is required', 400); } $id = $body['questionID']; - [$pdo, $tmpDb, $lockFp] = qdb_open(true); + [$pdo, $tmpDb, $lockFp] = qdb_open_write_or_fail(); try { $pdo->exec("PRAGMA foreign_keys = OFF;"); $pdo->beginTransaction(); @@ -202,12 +190,7 @@ case 'DELETE': qdb_save($tmpDb, $lockFp); json_success(['deleted' => true]); } catch (Throwable $e) { - if ($pdo->inTransaction()) { - $pdo->rollBack(); - } - qdb_discard($tmpDb, $lockFp); - error_log($e->getMessage()); - json_error('SERVER_ERROR', 'Server error', 500); + qdb_handler_fail($e, 'Delete question', $pdo ?? null, $tmpDb ?? null, $lockFp ?? null); } break; @@ -259,15 +242,7 @@ case 'PATCH': qdb_save($tmpDb, $lockFp); json_success(['reordered' => true]); } catch (Throwable $e) { - if (isset($tmpDb, $lockFp)) { - qdb_discard($tmpDb, $lockFp); - } - if (function_exists('qdb_api_log_note_exception')) { - qdb_api_log_note_exception($e); - } - require_once __DIR__ . '/../lib/errors.php'; - error_log('questions PATCH: ' . $e->getMessage()); - json_error('SERVER_ERROR', qdb_public_error_message($e, 'Reorder questions'), 500); + qdb_handler_fail($e, 'Reorder questions', null, $tmpDb ?? null, $lockFp ?? null); } break; diff --git a/handlers/results.php b/handlers/results.php index 3de8f5e..234c97d 100644 --- a/handlers/results.php +++ b/handlers/results.php @@ -120,13 +120,5 @@ if (!empty($questionIDs) && !empty($clients)) { 'clients' => $clients, ]); } catch (Throwable $e) { - if (isset($tmpDb, $lockFp)) { - qdb_discard($tmpDb, $lockFp); - } - if (function_exists('qdb_api_log_note_exception')) { - qdb_api_log_note_exception($e); - } - require_once __DIR__ . '/../lib/errors.php'; - error_log('results GET: ' . $e->getMessage()); - json_error('SERVER_ERROR', qdb_public_error_message($e, 'Load results'), 500); + qdb_handler_fail($e, 'Load results', null, $tmpDb ?? null, $lockFp ?? null); } diff --git a/handlers/session.php b/handlers/session.php index 7eebaad..7277f7a 100644 --- a/handlers/session.php +++ b/handlers/session.php @@ -34,15 +34,7 @@ if (($rec['userID'] ?? '') !== '') { $username = $row !== false ? (string)$row : ''; qdb_discard($tmpDb, $lockFp); } catch (Throwable $e) { - if (isset($tmpDb, $lockFp)) { - qdb_discard($tmpDb, $lockFp); - } - if (function_exists('qdb_api_log_note_exception')) { - qdb_api_log_note_exception($e); - } - require_once __DIR__ . '/../lib/errors.php'; - error_log('session username lookup: ' . $e->getMessage()); - json_error('SERVER_ERROR', qdb_public_error_message($e, 'Session check'), 500); + qdb_handler_fail($e, 'Session check', null, $tmpDb ?? null, $lockFp ?? null); } } diff --git a/handlers/translations.php b/handlers/translations.php index b9b3320..2c6b9b7 100644 --- a/handlers/translations.php +++ b/handlers/translations.php @@ -15,15 +15,7 @@ case 'GET': $bundle = qdb_export_translations_bundle($pdo); qdb_discard($tmpDb, $lockFp); } catch (Throwable $e) { - if (isset($tmpDb, $lockFp)) { - qdb_discard($tmpDb, $lockFp); - } - if (function_exists('qdb_api_log_note_exception')) { - qdb_api_log_note_exception($e); - } - require_once __DIR__ . '/../lib/errors.php'; - error_log('translations exportBundle: ' . $e->getMessage()); - json_error('SERVER_ERROR', qdb_public_error_message($e, 'Export'), 500); + qdb_handler_fail($e, 'Export translations', null, $tmpDb ?? null, $lockFp ?? null); } $filename = 'translations_bundle_' . date('Y-m-d_His') . '.json'; @@ -41,15 +33,7 @@ case 'GET': qdb_discard($tmpDb, $lockFp); json_success(['languages' => $rows]); } catch (Throwable $e) { - if (isset($tmpDb, $lockFp)) { - qdb_discard($tmpDb, $lockFp); - } - if (function_exists('qdb_api_log_note_exception')) { - qdb_api_log_note_exception($e); - } - require_once __DIR__ . '/../lib/errors.php'; - error_log('translations languages: ' . $e->getMessage()); - json_error('SERVER_ERROR', qdb_public_error_message($e, 'Load languages'), 500); + qdb_handler_fail($e, 'Load languages', null, $tmpDb ?? null, $lockFp ?? null); } } @@ -102,15 +86,7 @@ case 'GET': 'sourceLanguage' => QDB_SOURCE_LANGUAGE, ]); } catch (Throwable $e) { - if (isset($tmpDb, $lockFp)) { - qdb_discard($tmpDb, $lockFp); - } - if (function_exists('qdb_api_log_note_exception')) { - qdb_api_log_note_exception($e); - } - require_once __DIR__ . '/../lib/errors.php'; - error_log('translations all: ' . $e->getMessage()); - json_error('SERVER_ERROR', qdb_public_error_message($e, 'Load translations'), 500); + qdb_handler_fail($e, 'Load translations', null, $tmpDb ?? null, $lockFp ?? null); } } @@ -146,15 +122,7 @@ case 'GET': 'sourceLanguage' => QDB_SOURCE_LANGUAGE, ]); } catch (Throwable $e) { - if (isset($tmpDb, $lockFp)) { - qdb_discard($tmpDb, $lockFp); - } - if (function_exists('qdb_api_log_note_exception')) { - qdb_api_log_note_exception($e); - } - require_once __DIR__ . '/../lib/errors.php'; - error_log('translations questionnaire: ' . $e->getMessage()); - json_error('SERVER_ERROR', qdb_public_error_message($e, 'Load translations'), 500); + qdb_handler_fail($e, 'Load translations', null, $tmpDb ?? null, $lockFp ?? null); } } @@ -192,15 +160,7 @@ case 'GET': qdb_discard($tmpDb, $lockFp); json_success(['translations' => $rows]); } catch (Throwable $e) { - if (isset($tmpDb, $lockFp)) { - qdb_discard($tmpDb, $lockFp); - } - if (function_exists('qdb_api_log_note_exception')) { - qdb_api_log_note_exception($e); - } - require_once __DIR__ . '/../lib/errors.php'; - error_log('translations GET: ' . $e->getMessage()); - json_error('SERVER_ERROR', qdb_public_error_message($e, 'Load translations'), 500); + qdb_handler_fail($e, 'Load translations', null, $tmpDb ?? null, $lockFp ?? null); } break; @@ -212,20 +172,13 @@ case 'POST': if (!is_array($bundle)) { json_error('MISSING_FIELDS', 'bundle object is required', 400); } - [$pdo, $tmpDb, $lockFp] = qdb_open(true); + [$pdo, $tmpDb, $lockFp] = qdb_open_write_or_fail(); try { $result = qdb_import_translations_bundle($pdo, $bundle); qdb_save($tmpDb, $lockFp); json_success($result); } catch (Throwable $e) { - qdb_discard($tmpDb, $lockFp); - error_log('translations importBundle: ' . $e->getMessage()); - require_once __DIR__ . '/../lib/errors.php'; - if (function_exists('qdb_api_log_note_exception')) { - qdb_api_log_note_exception($e); - } - error_log('translations importBundle: ' . $e->getMessage()); - json_error('SERVER_ERROR', qdb_public_error_message($e, 'Import'), 500); + qdb_handler_fail($e, 'Import translations', null, $tmpDb, $lockFp); } break; } @@ -243,7 +196,7 @@ case 'PUT': if (!$lang) { json_error('MISSING_FIELDS', 'languageCode required', 400); } - [$pdo, $tmpDb, $lockFp] = qdb_open(true); + [$pdo, $tmpDb, $lockFp] = qdb_open_write_or_fail(); try { if ($lang === QDB_SOURCE_LANGUAGE) { qdb_ensure_source_language($pdo); @@ -255,9 +208,7 @@ case 'PUT': qdb_save($tmpDb, $lockFp); json_success([]); } catch (Throwable $e) { - qdb_discard($tmpDb, $lockFp); - error_log($e->getMessage()); - json_error('SERVER_ERROR', 'Server error', 500); + qdb_handler_fail($e, 'Save language', null, $tmpDb, $lockFp); } break; } @@ -280,15 +231,7 @@ case 'PUT': qdb_save($tmpDb, $lockFp); json_success([]); } catch (Throwable $e) { - if (isset($tmpDb, $lockFp)) { - qdb_discard($tmpDb, $lockFp); - } - if (function_exists('qdb_api_log_note_exception')) { - qdb_api_log_note_exception($e); - } - require_once __DIR__ . '/../lib/errors.php'; - error_log('translations PUT: ' . $e->getMessage()); - json_error('SERVER_ERROR', qdb_public_error_message($e, 'Save translation'), 500); + qdb_handler_fail($e, 'Save translation', null, $tmpDb ?? null, $lockFp ?? null); } break; @@ -305,7 +248,7 @@ case 'DELETE': if ($lang === QDB_SOURCE_LANGUAGE) { json_error('BAD_REQUEST', 'German (de) cannot be removed', 400); } - [$pdo, $tmpDb, $lockFp] = qdb_open(true); + [$pdo, $tmpDb, $lockFp] = qdb_open_write_or_fail(); try { $pdo->prepare("DELETE FROM language WHERE languageCode = :lc")->execute([':lc' => $lang]); $pdo->prepare("DELETE FROM question_translation WHERE languageCode = :lc")->execute([':lc' => $lang]); @@ -314,9 +257,7 @@ case 'DELETE': qdb_save($tmpDb, $lockFp); json_success([]); } catch (Throwable $e) { - qdb_discard($tmpDb, $lockFp); - error_log($e->getMessage()); - json_error('SERVER_ERROR', 'Server error', 500); + qdb_handler_fail($e, 'Delete language', null, $tmpDb, $lockFp); } break; } @@ -349,15 +290,7 @@ case 'DELETE': qdb_save($tmpDb, $lockFp); json_success([]); } catch (Throwable $e) { - if (isset($tmpDb, $lockFp)) { - qdb_discard($tmpDb, $lockFp); - } - if (function_exists('qdb_api_log_note_exception')) { - qdb_api_log_note_exception($e); - } - require_once __DIR__ . '/../lib/errors.php'; - error_log('translations DELETE: ' . $e->getMessage()); - json_error('SERVER_ERROR', qdb_public_error_message($e, 'Delete translation'), 500); + qdb_handler_fail($e, 'Delete translation', null, $tmpDb ?? null, $lockFp ?? null); } break; diff --git a/handlers/users.php b/handlers/users.php index 558a1ee..72e33f8 100644 --- a/handlers/users.php +++ b/handlers/users.php @@ -9,7 +9,7 @@ switch ($method) { case 'GET': try { - [$pdo, $tmpDb, $lockFp] = qdb_open(false); + [$pdo, $tmpDb, $lockFp] = qdb_open_read_or_fail(); if ($callerRole === 'admin') { $users = $pdo->query( @@ -53,9 +53,7 @@ case 'GET': 'callerRole' => $callerRole, ]); } catch (Throwable $e) { - if (isset($tmpDb, $lockFp)) qdb_discard($tmpDb, $lockFp); - error_log($e->getMessage()); - json_error('SERVER_ERROR', 'Server error', 500); + qdb_handler_fail($e, 'users', null, $tmpDb ?? null, $lockFp ?? null); } break; @@ -88,7 +86,7 @@ case 'POST': } try { - [$pdo, $tmpDb, $lockFp] = qdb_open(true); + [$pdo, $tmpDb, $lockFp] = qdb_open_write_or_fail(); $chk = $pdo->prepare("SELECT userID FROM users WHERE username = :u"); $chk->execute([':u' => $username]); @@ -142,7 +140,7 @@ case 'POST': $svUsername = null; if ($role === 'coach') { - [$pdo2, $tmp2, $lk2] = qdb_open(false); + [$pdo2, $tmp2, $lk2] = qdb_open_read_or_fail(); $svr = $pdo2->prepare("SELECT username FROM supervisor WHERE supervisorID = :sid"); $svr->execute([':sid' => $supervisorID]); $svUsername = $svr->fetchColumn() ?: null; @@ -161,10 +159,7 @@ case 'POST': 'createdAt' => $now, ]); } catch (Throwable $e) { - if (isset($pdo) && $pdo->inTransaction()) $pdo->rollBack(); - if (isset($tmpDb, $lockFp)) qdb_discard($tmpDb, $lockFp); - error_log($e->getMessage()); - json_error('SERVER_ERROR', 'Server error', 500); + qdb_handler_fail($e, 'users', $pdo ?? null, $tmpDb ?? null, $lockFp ?? null); } break; @@ -187,7 +182,7 @@ case 'PATCH': $mustChange = isset($body['mustChangePassword']) ? (int)(bool)$body['mustChangePassword'] : 1; try { - [$pdo, $tmpDb, $lockFp] = qdb_open(true); + [$pdo, $tmpDb, $lockFp] = qdb_open_write_or_fail(); $row = $pdo->prepare( "SELECT u.userID, u.username, u.role, u.entityID, c.supervisorID @@ -242,10 +237,8 @@ case 'PATCH': 'mustChangePassword' => $mustChange, ]); } catch (Throwable $e) { - if (isset($tmpDb, $lockFp)) qdb_discard($tmpDb, $lockFp); - error_log($e->getMessage()); - json_error('SERVER_ERROR', 'Server error', 500); - } + qdb_handler_fail($e, 'users', null, $tmpDb ?? null, $lockFp ?? null); + } break; } @@ -260,7 +253,7 @@ case 'PATCH': } try { - [$pdo, $tmpDb, $lockFp] = qdb_open(true); + [$pdo, $tmpDb, $lockFp] = qdb_open_write_or_fail(); $row = $pdo->prepare( "SELECT u.userID, u.username, u.role, u.entityID, c.supervisorID @@ -308,9 +301,7 @@ case 'PATCH': 'supervisorUsername' => $svUsername, ]); } catch (Throwable $e) { - if (isset($tmpDb, $lockFp)) qdb_discard($tmpDb, $lockFp); - error_log($e->getMessage()); - json_error('SERVER_ERROR', 'Server error', 500); + qdb_handler_fail($e, 'users', null, $tmpDb ?? null, $lockFp ?? null); } break; @@ -326,7 +317,7 @@ case 'DELETE': } try { - [$pdo, $tmpDb, $lockFp] = qdb_open(true); + [$pdo, $tmpDb, $lockFp] = qdb_open_write_or_fail(); $row = $pdo->prepare("SELECT role, entityID FROM users WHERE userID = :uid"); $row->execute([':uid' => $targetUserID]); @@ -368,10 +359,7 @@ case 'DELETE': json_success([]); } catch (Throwable $e) { - if (isset($pdo) && $pdo->inTransaction()) $pdo->rollBack(); - if (isset($tmpDb, $lockFp)) qdb_discard($tmpDb, $lockFp); - error_log($e->getMessage()); - json_error('SERVER_ERROR', 'Server error', 500); + qdb_handler_fail($e, 'users', $pdo ?? null, $tmpDb ?? null, $lockFp ?? null); } break; diff --git a/lib/errors.php b/lib/errors.php index 81f03fd..360cace 100644 --- a/lib/errors.php +++ b/lib/errors.php @@ -1,5 +1,16 @@ getMessage(); + return str_contains($msg, 'Could not acquire lock') + || str_contains($msg, 'Could not open lock file'); +} + /** * Map internal exceptions to safe, identifiable API error messages (details go to error_log). */ @@ -7,8 +18,8 @@ function qdb_public_error_message(Throwable $e, string $contextLabel = 'Operatio { $msg = $e->getMessage(); - if (str_contains($msg, 'Could not acquire lock') || str_contains($msg, 'Could not open lock')) { - return 'Database is busy. Please try again in a moment.'; + if (qdb_is_lock_exception($e)) { + return 'Database is busy because another update is in progress. Please try again in a moment.'; } if (str_contains($msg, 'decrypt') || str_contains($msg, 'QDB_MASTER_KEY')) { return 'Database configuration error. Contact the server administrator.'; @@ -23,3 +34,50 @@ function qdb_public_error_message(Throwable $e, string $contextLabel = 'Operatio return $contextLabel . ' failed. If this persists, contact support.'; } + +/** + * @return array{0: string, 1: string, 2: int} code, message, http status + */ +function qdb_api_error_for_exception(Throwable $e, string $contextLabel = 'Operation'): array +{ + if (qdb_is_lock_exception($e)) { + return ['LOCKED', qdb_public_error_message($e, $contextLabel), 503]; + } + return ['SERVER_ERROR', qdb_public_error_message($e, $contextLabel), 500]; +} + +/** + * Standard handler catch: rollback, discard DB temp file, log, JSON error (never returns). + */ +function qdb_handler_fail( + Throwable $e, + string $contextLabel, + ?PDO $pdo = null, + ?string $tmpDb = null, + $lockFp = null +): never { + if ($pdo !== null && $pdo->inTransaction()) { + try { + $pdo->rollBack(); + } catch (Throwable) { + } + } + if ($tmpDb !== null && $lockFp !== null) { + require_once __DIR__ . '/../db_init.php'; + qdb_discard($tmpDb, $lockFp); + } + qdb_emit_api_error($e, $contextLabel); +} + +/** + * Log exception and exit with unified JSON error (never returns). + */ +function qdb_emit_api_error(Throwable $e, string $contextLabel): never +{ + if (function_exists('qdb_api_log_note_exception')) { + qdb_api_log_note_exception($e); + } + error_log($contextLabel . ': ' . $e->getMessage()); + [$code, $message, $status] = qdb_api_error_for_exception($e, $contextLabel); + json_error($code, $message, $status); +} diff --git a/website/js/api.js b/website/js/api.js index 38be401..9d72ea5 100644 --- a/website/js/api.js +++ b/website/js/api.js @@ -172,12 +172,6 @@ export async function apiDelete(endpoint, body) { return unwrap(await res.json()); } -export function apiDownloadUrl(endpoint) { - const token = getToken(); - const cleanEndpoint = endpoint.replace(/\.php(\?|$)/, '$1'); - return `${API_BASE}/${cleanEndpoint}${cleanEndpoint.includes('?') ? '&' : '?'}token=${encodeURIComponent(token)}`; -} - /** Authenticated file/download fetch (marks request as website for activity logging). */ export async function apiDownloadFetch(url, options = {}) { const token = getToken();