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; } // Check 7: memory_limit / max_execution_time sanity (warnings, not fatal — a // low default doesn't fail requests until something large actually runs, so // surface it here rather than waiting for a mysterious failure under load). $memLimitIni = ini_get('memory_limit'); $memLimitUnit = strtolower(substr(trim($memLimitIni), -1)); $memLimitBytes = $memLimitIni === '-1' ? -1 : (int)$memLimitIni * match ($memLimitUnit) { 'g' => 1024 * 1024 * 1024, 'm' => 1024 * 1024, 'k' => 1024, default => 1, }; $minMemBytes = $requirements['min_memory_limit_mb'] * 1024 * 1024; if ($memLimitBytes === -1 || $memLimitBytes >= $minMemBytes) { $checks['memory_limit'] = ['status' => 'ok', 'message' => $memLimitIni]; } else { $checks['memory_limit'] = [ 'status' => 'warning', 'message' => sprintf('%s is below the recommended minimum %dM', $memLimitIni, $requirements['min_memory_limit_mb']) ]; } $maxExecTime = (int)ini_get('max_execution_time'); if ($maxExecTime === 0 || $maxExecTime >= $requirements['min_max_execution_time']) { $checks['max_execution_time'] = ['status' => 'ok', 'message' => (string)$maxExecTime]; } else { $checks['max_execution_time'] = [ 'status' => 'warning', 'message' => sprintf('%ds is below the recommended minimum %ds', $maxExecTime, $requirements['min_max_execution_time']) ]; } // 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);