$headers * @return array{ok: bool, status: int, data?: mixed, error?: array} */ public function request( string $method, string $route, ?array $body = null, array $headers = [], array $query = [], ): array { qdb_test_reset_http_state(); $_SERVER['REQUEST_METHOD'] = strtoupper($method); $queryString = $query !== [] ? '?' . http_build_query($query) : ''; $_SERVER['REQUEST_URI'] = '/api/' . ltrim($route, '/') . $queryString; $_SERVER['SCRIPT_NAME'] = '/api/index.php'; $_GET = $query; if ($queryString !== '') { parse_str(ltrim($queryString, '?'), $_GET); } unset($_SERVER['HTTP_AUTHORIZATION'], $_SERVER['Authorization']); unset($_SERVER['HTTP_X_QDB_CLIENT']); foreach ($headers as $name => $value) { $key = 'HTTP_' . strtoupper(str_replace('-', '_', $name)); $_SERVER[$key] = $value; } if ($body !== null) { qdb_test_set_request_body(json_encode($body, JSON_THROW_ON_ERROR)); } ob_start(); try { require $this->apiIndexPath; ob_end_clean(); return ['ok' => false, 'status' => 500, 'error' => ['code' => 'NO_RESPONSE', 'message' => 'Handler did not return JSON']]; } catch (JsonSuccessResponse $e) { $output = ob_get_clean(); return [ 'ok' => true, 'status' => $e->httpStatus, 'data' => $e->data, '_raw' => $output, ]; } catch (JsonErrorResponse $e) { ob_get_clean(); $err = ['code' => $e->errorCode, 'message' => $e->errorMessage]; if ($e->details !== null) { $err['details'] = $e->details; } return [ 'ok' => false, 'status' => $e->httpStatus, 'error' => $err, ]; } catch (RawHttpResponse $e) { ob_get_clean(); return [ 'ok' => true, 'status' => $e->httpStatus, 'raw' => true, 'body' => $e->body, 'headers' => $e->headers, ]; } catch (\Throwable $e) { ob_end_clean(); throw $e; } } public function loginWeb(string $username, string $password): array { return $this->request('POST', 'auth/login', [ 'username' => $username, 'password' => $password, ], ['X-QDB-Client' => 'web']); } public function loginMobile(string $username, string $password): array { return $this->request('POST', 'auth/login', [ 'username' => $username, 'password' => $password, ]); } /** * @return array{token: string, data: array} */ public function loginMobileAndGetToken(string $username, string $password): array { $res = $this->loginMobile($username, $password); if (!$res['ok']) { throw new \RuntimeException('Login failed: ' . ($res['error']['message'] ?? 'unknown')); } $token = $res['data']['token'] ?? ''; if ($token === '') { throw new \RuntimeException('Login response missing token'); } return ['token' => $token, 'data' => $res['data']]; } /** * @return array{token: string, data: array} */ public function loginWebAndGetToken(string $username, string $password): array { $res = $this->loginWeb($username, $password); if (!$res['ok']) { throw new \RuntimeException('Login failed: ' . ($res['error']['message'] ?? 'unknown')); } $token = $res['data']['token'] ?? ''; if ($token === '') { throw new \RuntimeException('Login response missing token'); } return ['token' => $token, 'data' => $res['data']]; } /** * @param array $extraHeaders */ public function withToken(string $token, string $method, string $route, ?array $body = null, array $query = [], array $extraHeaders = []): array { $headers = array_merge([ 'Authorization' => 'Bearer ' . $token, 'X-QDB-Client' => 'web', ], $extraHeaders); return $this->request($method, $route, $body, $headers, $query); } /** * @param array $extraHeaders */ public function withMobileToken( string $token, string $method, string $route, ?array $body = null, array $query = [], array $extraHeaders = [], ): array { $headers = array_merge(['Authorization' => 'Bearer ' . $token], $extraHeaders); return $this->request($method, $route, $body, $headers, $query); } /** * POST an encrypted payload (Android app API). * * @param array $payload */ public function encryptedPost(string $token, string $route, array $payload): array { $envelope = qdb_sensitive_envelope( json_encode($payload, JSON_THROW_ON_ERROR), $token ); return $this->withMobileToken($token, 'POST', $route, $envelope); } }