diff --git a/cli/diagnose_db.php b/cli/diagnose_db.php index 17662d8..c3f3ae2 100644 --- a/cli/diagnose_db.php +++ b/cli/diagnose_db.php @@ -12,6 +12,23 @@ if (php_sapi_name() !== 'cli') { require_once __DIR__ . '/../common.php'; require_once __DIR__ . '/../db_init.php'; +/** @return array{ok: bool, error?: string} */ +function qdb_probe_decrypt(string $dbPath, string $keyBytes): array { + if (!is_file($dbPath)) { + return ['ok' => false, 'error' => 'file missing']; + } + $storedEnc = @file_get_contents($dbPath); + if ($storedEnc === false || strlen($storedEnc) < 17) { + return ['ok' => false, 'error' => 'unreadable or too short']; + } + try { + aes256_cbc_decrypt_bytes($storedEnc, $keyBytes); + return ['ok' => true]; + } catch (Throwable $e) { + return ['ok' => false, 'error' => $e->getMessage()]; + } +} + $envPath = __DIR__ . '/../.env'; echo "Project: " . realpath(__DIR__ . '/..') . "\n"; echo ".env: " . ($envPath && is_readable($envPath) ? "readable" : "MISSING or not readable") . "\n"; @@ -22,6 +39,8 @@ if ($keyEnv === false || $keyEnv === '') { exit(1); } echo "QDB_MASTER_KEY: set (" . strlen($keyEnv) . " chars)\n"; +$b64 = base64_decode($keyEnv, true); +echo " parses as base64: " . ($b64 !== false && strlen($b64) > 0 ? 'yes (' . strlen($b64) . ' bytes)' : 'no (uses first 32 ASCII chars)') . "\n"; $dbPath = QDB_PATH; $uploads = dirname($dbPath); @@ -29,7 +48,68 @@ echo "uploads dir: $uploads\n"; echo " exists: " . (is_dir($uploads) ? 'yes' : 'no') . "\n"; echo " writable: " . (is_writable($uploads) ? 'yes' : 'NO — migrations cannot save') . "\n"; echo "DB file: $dbPath\n"; -echo " exists: " . (is_file($dbPath) ? 'yes (' . filesize($dbPath) . ' bytes)' : 'no (will create on first write)') . "\n"; +if (is_file($dbPath)) { + $mtime = filemtime($dbPath); + echo " exists: yes (" . filesize($dbPath) . " bytes)\n"; + echo " modified: " . ($mtime ? date('Y-m-d H:i:s T', $mtime) : '?') . "\n"; +} else { + echo " exists: no (will create on first write)\n"; +} + +$backupDir = $uploads . '/backups'; +if (is_dir($backupDir)) { + $backups = glob($backupDir . '/questionnaire_database.*') ?: []; + rsort($backups); + echo "Backups in uploads/backups: " . count($backups) . "\n"; + foreach (array_slice($backups, 0, 5) as $bp) { + $mt = filemtime($bp); + echo " - " . basename($bp) . ' (' . filesize($bp) . " bytes, " . ($mt ? date('Y-m-d H:i:s', $mt) : '?') . ")\n"; + } +} else { + echo "Backups: none (uploads/backups missing)\n"; +} + +if (!is_file($dbPath)) { + exit(0); +} + +$candidates = [ + 'current .env (get_master_key_bytes)' => get_master_key_bytes(), + 'legacy fallback (pre-April 2026 default)' => str_pad('12345678901234567890123456789012', 32, "\0"), + 'raw first 32 chars of QDB_MASTER_KEY' => str_pad(substr($keyEnv, 0, 32), 32, "\0"), +]; +if ($b64 !== false && strlen($b64) > 0) { + $candidates['base64-decoded .env only'] = str_pad(substr($b64, 0, 32), 32, "\0"); +} + +echo "\nDecrypt probe (which key matches questionnaire_database?):\n"; +$matched = null; +foreach ($candidates as $label => $keyBytes) { + $probe = qdb_probe_decrypt($dbPath, $keyBytes); + if ($probe['ok']) { + echo " MATCH: $label\n"; + $matched = $label; + } else { + echo " no: $label\n"; + } +} + +if ($matched === null) { + echo "\nNo known key opened the file. Likely causes:\n"; + echo " - DB was encrypted with a different QDB_MASTER_KEY than in .env now\n"; + echo " - File was corrupted (e.g. interrupted save); try an older uploads/backups copy\n"; + echo " - Wrong DB path (another install under /var/www/...)\n"; + echo "\nWiping is NOT required for schema updates. Only wipe if you accept data loss\n"; + echo "and run: mv uploads/questionnaire_database uploads/questionnaire_database.bak\n"; + echo "then: php seed_admin.php \n"; + exit(1); +} + +if ($matched !== 'current .env (get_master_key_bytes)') { + echo "\nThe DB does NOT match your current .env key but matches: $matched\n"; + echo "Fix: put the matching key in .env, open the DB once, then re-save — or restore a backup.\n"; + exit(1); +} try { [$pdo, $tmpDb, $lockFp] = qdb_open(false); @@ -37,13 +117,12 @@ try { $users = (int)$pdo->query('SELECT COUNT(*) FROM users')->fetchColumn(); $hasSession = qdb_table_exists($pdo, 'session'); qdb_discard($tmpDb, $lockFp); - echo "OK: database opened and decrypted.\n"; + echo "\nOK: full open + migrations succeeded.\n"; echo " PRAGMA user_version: $version (expected " . QDB_VERSION . ")\n"; echo " users: $users\n"; echo " session table: " . ($hasSession ? 'yes' : 'MISSING') . "\n"; exit(0); } catch (Throwable $e) { - echo "FAILED: " . $e->getMessage() . "\n"; - echo " (Check Apache/PHP error_log for stack trace.)\n"; + echo "\nDecrypt OK but qdb_open failed: " . $e->getMessage() . "\n"; exit(1); }