$message]; if ($details !== null) { $body['errors'] = $details; } json_response($body, $status); } // -------------------------------------------------------------------------- // Request parsing // -------------------------------------------------------------------------- /** * Parse the JSON request body. * * @return array Decoded JSON as an associative array. */ function get_json_body(): array { $raw = file_get_contents('php://input'); if (empty($raw)) { return []; } $data = json_decode($raw, true); if (json_last_error() !== JSON_ERROR_NONE) { error_response('Invalid JSON in request body', 400); } return $data; } /** * Get the client IP address, accounting for reverse proxies. * * Checks X-Forwarded-For first, then X-Real-IP, then REMOTE_ADDR. * * @return string|null */ function get_client_ip(): ?string { if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) { $parts = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']); return trim($parts[0]); } if (!empty($_SERVER['HTTP_X_REAL_IP'])) { return trim($_SERVER['HTTP_X_REAL_IP']); } return $_SERVER['REMOTE_ADDR'] ?? null; } /** * Get the User-Agent header value. * * @return string|null */ function get_user_agent(): ?string { return $_SERVER['HTTP_USER_AGENT'] ?? null; } // -------------------------------------------------------------------------- // CORS // -------------------------------------------------------------------------- /** * Emit CORS headers based on the configured allowed origins. * * For preflight (OPTIONS) requests, this also sets the allowed methods * and headers, then terminates the script with 204. */ function cors_headers(): void { $origin = $_SERVER['HTTP_ORIGIN'] ?? ''; $allowed = array_map('trim', explode(',', CORS_ALLOWED_ORIGINS)); // Allow the origin if it matches our whitelist, or allow all if '*' if (in_array('*', $allowed, true) || in_array($origin, $allowed, true)) { $send_origin = in_array('*', $allowed, true) ? '*' : $origin; header("Access-Control-Allow-Origin: {$send_origin}"); } header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS'); header('Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With'); header('Access-Control-Max-Age: 86400'); // Handle preflight if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(204); exit; } } // -------------------------------------------------------------------------- // Validation // -------------------------------------------------------------------------- /** * Validate that all required fields are present and non-empty in the data. * * @param array $data Associative array of input data. * @param string[] $fields List of required field names. * @return string[] Array of error messages (empty if valid). */ function validate_required(array $data, array $fields): array { $errors = []; foreach ($fields as $field) { if (!isset($data[$field]) || (is_string($data[$field]) && trim($data[$field]) === '')) { $errors[] = "Field '{$field}' is required."; } } return $errors; } /** * Validate an email address. * * @param string $email Email address to validate. * @return bool True if valid. */ function validate_email(string $email): bool { return filter_var($email, FILTER_VALIDATE_EMAIL) !== false; } // -------------------------------------------------------------------------- // Logging // -------------------------------------------------------------------------- /** * Append a message to the application log file. * * @param string $level Log level (INFO, WARNING, ERROR). * @param string $message Log message. */ function app_log(string $level, string $message): void { $dir = dirname(LOG_FILE); if (!is_dir($dir)) { @mkdir($dir, 0750, true); } $timestamp = gmdate('Y-m-d\TH:i:s\Z'); $line = "[{$timestamp}] [{$level}] {$message}" . PHP_EOL; @file_put_contents(LOG_FILE, $line, FILE_APPEND | LOCK_EX); } // -------------------------------------------------------------------------- // Datetime helpers // -------------------------------------------------------------------------- /** * Format a datetime value for JSON output (ISO 8601 format). * * Accepts a datetime string from MySQL (Y-m-d H:i:s) and returns * an ISO 8601 string, or null if input is null/empty. * * @param string|null $dt MySQL datetime string. * @return string|null ISO 8601 formatted string. */ function format_datetime(?string $dt): ?string { if ($dt === null || $dt === '' || $dt === '0000-00-00 00:00:00') { return null; } // MySQL DATETIME is already in UTC for this application $ts = strtotime($dt); if ($ts === false) { return null; } return gmdate('Y-m-d\TH:i:s\Z', $ts); } /** * Get the current UTC datetime in MySQL format. * * @return string Y-m-d H:i:s */ function utc_now(): string { return gmdate('Y-m-d H:i:s'); }