- create_ticket_api.php: remove the wrong CREATE TABLE stub that broke a fresh DB; generate collision-safe ticket_ids so a genuine id collision isn't misreported as a duplicate and a hw alert dropped; stop leaking raw DB errors; correct a reopen comment that falsely claimed refreshed sensor data - manage_recurring.php: fix next-run so create/edit no longer skips the current period (monthly day-of-month this month, daily today if time not passed, correct ISO weekday, month-length clamp); only recompute on schedule changes to avoid double-fire - export_tickets.php, audit_log.php: neutralize CSV formula injection - revoke_api_key.php, generate_api_key.php: correct HTTP status codes and stop the catch clobbering specific 4xx codes - health.php: stop leaking PHP version / extension names / paths to unauthenticated callers - watch_ticket.php: define $data before use - manage_templates/recurring/custom_fields: add audit logging for CRUD; add recurring_ticket + custom_field to the audit entity whitelist Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
154 lines
4.3 KiB
PHP
154 lines
4.3 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Health Check Endpoint
|
|
*
|
|
* Returns system health status for monitoring tools.
|
|
* Does not require authentication - suitable for load balancer health checks.
|
|
*
|
|
* Returns:
|
|
* - 200 OK: System is healthy
|
|
* - 503 Service Unavailable: System has issues
|
|
*/
|
|
|
|
// Don't apply rate limiting to health checks - they should always respond
|
|
header('Content-Type: application/json');
|
|
header('Cache-Control: no-cache, no-store, must-revalidate');
|
|
|
|
$startTime = microtime(true);
|
|
$checks = [];
|
|
$healthy = true;
|
|
|
|
// Check 1: Database connectivity
|
|
try {
|
|
require_once dirname(__DIR__) . '/config/config.php';
|
|
require_once dirname(__DIR__) . '/helpers/Database.php';
|
|
|
|
$conn = Database::getConnection();
|
|
|
|
// Quick query to verify connection is actually working
|
|
$result = $conn->query('SELECT 1');
|
|
if ($result && $result->fetch_row()) {
|
|
$checks['database'] = [
|
|
'status' => 'ok',
|
|
'message' => 'Connected'
|
|
];
|
|
} else {
|
|
$checks['database'] = [
|
|
'status' => 'error',
|
|
'message' => 'Query failed'
|
|
];
|
|
$healthy = false;
|
|
}
|
|
} catch (Exception $e) {
|
|
$checks['database'] = [
|
|
'status' => 'error',
|
|
'message' => 'Connection failed'
|
|
];
|
|
$healthy = false;
|
|
}
|
|
|
|
// Check 2: File system (uploads directory writable)
|
|
$uploadDir = $GLOBALS['config']['UPLOAD_DIR'] ?? dirname(__DIR__) . '/uploads';
|
|
if (is_dir($uploadDir) && is_writable($uploadDir)) {
|
|
$checks['filesystem'] = [
|
|
'status' => 'ok',
|
|
'message' => 'Writable'
|
|
];
|
|
} else {
|
|
$checks['filesystem'] = [
|
|
'status' => 'warning',
|
|
'message' => 'Upload directory not writable'
|
|
];
|
|
// Don't mark as unhealthy - this might be intentional
|
|
}
|
|
|
|
// Check 3: Session storage
|
|
$sessionPath = session_save_path() ?: sys_get_temp_dir();
|
|
if (is_dir($sessionPath) && is_writable($sessionPath)) {
|
|
$checks['sessions'] = [
|
|
'status' => 'ok',
|
|
'message' => 'Writable'
|
|
];
|
|
} else {
|
|
$checks['sessions'] = [
|
|
'status' => 'error',
|
|
'message' => 'Session storage not writable'
|
|
];
|
|
$healthy = false;
|
|
}
|
|
|
|
// Check 4: Rate limit storage
|
|
$rateLimitDir = sys_get_temp_dir() . '/tinker_tickets_ratelimit';
|
|
if (!is_dir($rateLimitDir)) {
|
|
@mkdir($rateLimitDir, 0755, true);
|
|
}
|
|
if (is_dir($rateLimitDir) && is_writable($rateLimitDir)) {
|
|
$checks['rate_limit'] = [
|
|
'status' => 'ok',
|
|
'message' => 'Writable'
|
|
];
|
|
} else {
|
|
$checks['rate_limit'] = [
|
|
'status' => 'warning',
|
|
'message' => 'Rate limit storage not writable'
|
|
];
|
|
}
|
|
|
|
// Check 5: Required PHP extensions (catches e.g. a PHP upgrade silently
|
|
// dropping php-ldap, which breaks avatars with no other visible error).
|
|
$requirements = require dirname(__DIR__) . '/config/requirements.php';
|
|
$missingExt = array_values(array_filter(
|
|
$requirements['required_extensions'],
|
|
fn($ext) => !extension_loaded($ext)
|
|
));
|
|
if (empty($missingExt)) {
|
|
$checks['php_extensions'] = [
|
|
'status' => 'ok',
|
|
'message' => 'All required extensions loaded'
|
|
];
|
|
} else {
|
|
$checks['php_extensions'] = [
|
|
'status' => 'error',
|
|
'message' => 'Missing extensions: ' . implode(', ', $missingExt)
|
|
];
|
|
$healthy = false;
|
|
}
|
|
|
|
// Check 6: PHP version meets the declared minimum
|
|
if (version_compare(PHP_VERSION, $requirements['min_php_version'], '>=')) {
|
|
$checks['php_version'] = [
|
|
'status' => 'ok',
|
|
'message' => PHP_VERSION
|
|
];
|
|
} else {
|
|
$checks['php_version'] = [
|
|
'status' => 'error',
|
|
'message' => sprintf('PHP %s < required %s', PHP_VERSION, $requirements['min_php_version'])
|
|
];
|
|
$healthy = false;
|
|
}
|
|
|
|
// Calculate response time
|
|
$responseTime = round((microtime(true) - $startTime) * 1000, 2);
|
|
|
|
// Set status code
|
|
http_response_code($healthy ? 200 : 503);
|
|
|
|
// This endpoint is unauthenticated, so expose only a coarse per-component status
|
|
// and never the diagnostic messages (they leak PHP_VERSION, exact missing
|
|
// extension names, and filesystem paths to anonymous callers).
|
|
$publicChecks = [];
|
|
foreach ($checks as $name => $check) {
|
|
$publicChecks[$name] = ['status' => $check['status']];
|
|
}
|
|
|
|
// Return response
|
|
echo json_encode([
|
|
'status' => $healthy ? 'healthy' : 'unhealthy',
|
|
'timestamp' => date('c'),
|
|
'response_time_ms' => $responseTime,
|
|
'checks' => $publicChecks,
|
|
'version' => '1.0.0'
|
|
], JSON_PRETTY_PRINT);
|