Route index.php and create_ticket_api.php through Database::getConnection() (#103, #104)

Both files opened their own raw new mysqli(...) connection instead of
using Database::getConnection(), missing the charset/timezone sync
every other connection gets. index.php's connection serves nearly all
non-API web traffic (dashboard, ticket view, ticket create) — any
NOW()/CURDATE()-based query through it used the DB server's default
session timezone instead of the app's configured TIMEZONE, and no
explicit utf8mb4 charset meant multi-byte characters typed into a
ticket via the non-JS POST fallback could get corrupted at write time.
create_ticket_api.php (the hwmonDaemon Bearer endpoint) had the same
timezone gap, risking created_at landing on the wrong 'day' relative
to every other ticket-creation path.

index.php's raw die("Connection failed: " . $conn->connect_error) also
leaked raw mysqli error text (host/user/failure reason) to any
unauthenticated visitor on a DB outage; it now logs via error_log()
and shows a generic message instead. create_ticket_api.php already
handled this correctly (JSON error + error_log, no leak) and needed no
behavior change there beyond the connection source.

Verified against real MariaDB: the new connection path reports the
configured -04:00 session time_zone and utf8mb4 charset, vs. SYSTEM
tz on the old raw-mysqli path; a simulated connection failure (bad
DB_NAME) confirmed only the generic message reaches the response body
while the raw driver error goes to error_log().

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lhz7pGMaoTfL5sdYS5XiKv
This commit is contained in:
2026-09-11 11:42:21 -04:00
co-authored by Claude Sonnet 5
parent 99a0cf59a8
commit 98d30cbc58
2 changed files with 25 additions and 49 deletions
+17 -39
View File
@@ -8,9 +8,10 @@ ini_set('display_errors', 0);
require_once __DIR__ . '/middleware/RateLimitMiddleware.php'; require_once __DIR__ . '/middleware/RateLimitMiddleware.php';
RateLimitMiddleware::apply('api'); RateLimitMiddleware::apply('api');
// Load environment variables with error check // Early friendly JSON error if .env is missing, before config.php's own
$envFile = __DIR__ . '/.env'; // (plain-text die()) handling would otherwise run — this is a JSON API
if (!file_exists($envFile)) { // endpoint and must always respond with a JSON body.
if (!file_exists(__DIR__ . '/.env')) {
echo json_encode([ echo json_encode([
'success' => false, 'success' => false,
'error' => 'Configuration file not found' 'error' => 'Configuration file not found'
@@ -18,37 +19,17 @@ if (!file_exists($envFile)) {
exit; exit;
} }
$envVars = parse_ini_file($envFile, false, INI_SCANNER_TYPED); // Load application config so UrlHelper can resolve APP_DOMAIN, and so the
if (!$envVars) { // DB connection below (via Database::getConnection()) gets the same
echo json_encode([ // charset/timezone sync as every other endpoint instead of a hand-rolled
'success' => false, // second connection.
'error' => 'Invalid configuration file' require_once __DIR__ . '/config/config.php';
]); require_once __DIR__ . '/helpers/Database.php';
exit;
}
// Strip quotes from values if present (parse_ini_file may include them) try {
foreach ($envVars as $key => $value) { $conn = Database::getConnection();
if (is_string($value)) { } catch (\Throwable $e) {
if ( error_log('create_ticket_api: DB connection failed: ' . $e->getMessage());
(substr($value, 0, 1) === '"' && substr($value, -1) === '"') ||
(substr($value, 0, 1) === "'" && substr($value, -1) === "'")
) {
$envVars[$key] = substr($value, 1, -1);
}
}
}
// Database connection with detailed error handling
$conn = new mysqli(
$envVars['DB_HOST'],
$envVars['DB_USER'],
$envVars['DB_PASS'],
$envVars['DB_NAME']
);
if ($conn->connect_error) {
error_log('create_ticket_api: DB connection failed: ' . $conn->connect_error);
http_response_code(500); http_response_code(500);
echo json_encode([ echo json_encode([
'success' => false, 'success' => false,
@@ -57,9 +38,6 @@ if ($conn->connect_error) {
exit; exit;
} }
// Load application config so UrlHelper can resolve APP_DOMAIN
require_once __DIR__ . '/config/config.php';
// Authenticate via API key // Authenticate via API key
require_once __DIR__ . '/middleware/ApiKeyAuth.php'; require_once __DIR__ . '/middleware/ApiKeyAuth.php';
require_once __DIR__ . '/models/AuditLogModel.php'; require_once __DIR__ . '/models/AuditLogModel.php';
@@ -349,7 +327,7 @@ if ($existing) {
(new StatsModel($conn))->invalidateCache(); (new StatsModel($conn))->invalidateCache();
} }
$conn->close(); Database::close();
echo json_encode([ echo json_encode([
'success' => true, 'success' => true,
'ticket_id' => $existingId, 'ticket_id' => $existingId,
@@ -386,7 +364,7 @@ if ($existing) {
// Ticket reopened (Closed → Open) — refresh dashboard stats. // Ticket reopened (Closed → Open) — refresh dashboard stats.
(new StatsModel($conn))->invalidateCache(); (new StatsModel($conn))->invalidateCache();
$conn->close(); Database::close();
require_once __DIR__ . '/helpers/NotificationHelper.php'; require_once __DIR__ . '/helpers/NotificationHelper.php';
NotificationHelper::sendTicketNotification($existingId, [ NotificationHelper::sendTicketNotification($existingId, [
@@ -484,7 +462,7 @@ if ($inserted) {
// New ticket created — refresh dashboard stats. // New ticket created — refresh dashboard stats.
(new StatsModel($conn))->invalidateCache(); (new StatsModel($conn))->invalidateCache();
$conn->close(); Database::close();
require_once __DIR__ . '/helpers/NotificationHelper.php'; require_once __DIR__ . '/helpers/NotificationHelper.php';
NotificationHelper::sendTicketNotification($ticket_id, [ NotificationHelper::sendTicketNotification($ticket_id, [
+8 -10
View File
@@ -5,6 +5,7 @@ require_once 'config/config.php';
require_once 'middleware/SecurityHeadersMiddleware.php'; require_once 'middleware/SecurityHeadersMiddleware.php';
require_once 'middleware/AuthMiddleware.php'; require_once 'middleware/AuthMiddleware.php';
require_once 'models/AuditLogModel.php'; require_once 'models/AuditLogModel.php';
require_once 'helpers/Database.php';
// Apply security headers early // Apply security headers early
SecurityHeadersMiddleware::apply(); SecurityHeadersMiddleware::apply();
@@ -17,15 +18,12 @@ $requestPath = strtok($request, '?');
// Create database connection for non-API routes // Create database connection for non-API routes
if (!str_starts_with($requestPath, '/api/')) { if (!str_starts_with($requestPath, '/api/')) {
$conn = new mysqli( try {
$GLOBALS['config']['DB_HOST'], $conn = Database::getConnection();
$GLOBALS['config']['DB_USER'], } catch (\Throwable $e) {
$GLOBALS['config']['DB_PASS'], error_log('index.php: database connection failed: ' . $e->getMessage());
$GLOBALS['config']['DB_NAME'] http_response_code(500);
); die('Sorry, something went wrong. Please try again shortly.');
if ($conn->connect_error) {
die("Connection failed: " . $conn->connect_error);
} }
// Authenticate user via Authelia forward auth // Authenticate user via Authelia forward auth
@@ -444,5 +442,5 @@ switch (true) {
// Close database connection if it was opened // Close database connection if it was opened
if (isset($conn)) { if (isset($conn)) {
$conn->close(); Database::close();
} }