PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false, ] ); // Idempotent migrations — safe to run on every boot $pdo->exec('ALTER TABLE delivery_log ADD COLUMN IF NOT EXISTS is_sandbox TINYINT(1) NOT NULL DEFAULT 0'); $pdo->exec("INSERT IGNORE INTO `system` (setting_name, setting_value) VALUES ('sandbox_mode', '0')"); return $pdo; } /** * Sends a JSON response and exits. */ function json_out(mixed $data, int $status = 200): never { http_response_code($status); header('Content-Type: application/json; charset=utf-8'); echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); exit; } /** * Decodes the request body as JSON and returns an array. */ function json_body(): array { $raw = file_get_contents('php://input'); if ($raw === false || $raw === '') { return []; } return json_decode($raw, true) ?? []; } /** * Sends a JSON error response and exits. */ function error_out(string $message, int $status = 400): never { json_out(['error' => $message], $status); } /** * Aborts with 400 if no Clickatell API key has been configured. */ function require_api_key(): void { $stmt = db()->prepare("SELECT setting_value FROM `system` WHERE setting_name = 'clickatell_api_key'"); $stmt->execute(); $key = $stmt->fetchColumn(); if ($key === false || trim((string) $key) === '') { error_out('No Clickatell API key is configured. Set one in Admin → Clickatell Settings before sending or scheduling messages.', 400); } }