status = $status; $this->errorCode = $errorCode; $this->extra = $extra; } } function dbnToolsLoadEnv(string $path): void { if (!is_file($path) || !is_readable($path)) { return; } $lines = file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); if ($lines === false) { return; } foreach ($lines as $line) { $line = trim($line); if ($line === '' || str_starts_with($line, '#') || !str_contains($line, '=')) { continue; } [$key, $value] = explode('=', $line, 2); $key = trim($key); $value = trim($value); if ($key === '') { continue; } if ((str_starts_with($value, '"') && str_ends_with($value, '"')) || (str_starts_with($value, "'") && str_ends_with($value, "'"))) { $value = substr($value, 1, -1); } if (getenv($key) === false) { putenv($key . '=' . $value); $_ENV[$key] = $value; } } } dbnToolsLoadEnv(DBN_TOOLS_ROOT . '/.env'); function dbnToolsEnv(string $key, ?string $default = null): ?string { $fileKey = $key . '_FILE'; $filePath = getenv($fileKey); if ($filePath !== false && $filePath !== '') { $value = @file_get_contents($filePath); if ($value === false) { throw new RuntimeException("Unable to read secret file for {$fileKey}"); } return rtrim($value, "\r\n"); } $value = getenv($key); if ($value === false || $value === '') { return $default; } return $value; } function dbnToolsIsHttps(): bool { if (!empty($_SERVER['HTTPS']) && strtolower((string)$_SERVER['HTTPS']) !== 'off') { return true; } return isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && strtolower((string)$_SERVER['HTTP_X_FORWARDED_PROTO']) === 'https'; } function dbnToolsStartSession(): void { if (session_status() === PHP_SESSION_ACTIVE) { return; } session_name('dbn_tools_session'); session_set_cookie_params([ 'lifetime' => 0, 'path' => '/', 'secure' => dbnToolsIsHttps(), 'httponly' => true, 'samesite' => 'Lax', ]); session_start(); if (empty($_SESSION['dbn_tools_anon_id'])) { $_SESSION['dbn_tools_anon_id'] = bin2hex(random_bytes(16)); } } dbnToolsStartSession(); function dbnToolsIsAuthenticated(): bool { return !empty($_SESSION['dbn_tools_authenticated']); } function dbnToolsAuthEmail(): ?string { return dbnToolsEnv('DBN_TOOLS_AUTH_EMAIL'); } function dbnToolsAuthPasswordHash(): ?string { return dbnToolsEnv('DBN_TOOLS_AUTH_PASSWORD_HASH'); } function dbnToolsAnonymousSessionId(): string { $id = (string)($_SESSION['dbn_tools_anon_id'] ?? ''); if ($id === '') { $id = bin2hex(random_bytes(16)); $_SESSION['dbn_tools_anon_id'] = $id; } return substr(hash('sha256', $id), 0, 18); } function dbnToolsRespond(array $payload, int $status = 200): void { http_response_code($status); header('Content-Type: application/json; charset=utf-8'); header('Cache-Control: no-store'); echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); exit; } function dbnToolsError(string $message, int $status = 400, string $code = 'bad_request', array $extra = []): void { dbnToolsRespond(array_merge([ 'ok' => false, 'error' => [ 'code' => $code, 'message' => $message, ], ], $extra), $status); } function dbnToolsAbort(string $message, int $status = 400, string $code = 'bad_request', array $extra = []): void { throw new DbnToolsHttpException($message, $status, $code, $extra); } function dbnToolsRequireMethod(string $method): void { if (strtoupper($_SERVER['REQUEST_METHOD'] ?? 'GET') !== strtoupper($method)) { dbnToolsError('Method not allowed.', 405, 'method_not_allowed'); } } function dbnToolsRequireAuth(): void { if (!dbnToolsIsAuthenticated()) { dbnToolsError('Passcode session required.', 401, 'session_required'); } } function dbnToolsJsonInput(int $maxBytes = 50000): array { $raw = file_get_contents('php://input'); if ($raw === false) { dbnToolsError('Unable to read request body.', 400, 'body_unreadable'); } if (strlen($raw) > $maxBytes) { dbnToolsError('Request body is too large for this tool.', 413, 'body_too_large'); } $data = json_decode($raw, true); if (!is_array($data)) { dbnToolsError('Request body must be valid JSON.', 400, 'invalid_json'); } return $data; } function dbnToolsNormalizeLanguage(mixed $value): string { $language = strtolower(trim((string)$value)); return in_array($language, ['no', 'en'], true) ? $language : 'en'; } function dbnToolsString(array $input, string $key, int $maxChars, bool $required = true): string { $value = trim((string)($input[$key] ?? '')); if ($required && $value === '') { dbnToolsAbort("Missing required field: {$key}.", 422, 'missing_field'); } if (mb_strlen($value, 'UTF-8') > $maxChars) { dbnToolsAbort("Field {$key} is too long.", 422, 'field_too_long'); } return $value; } function dbnToolsSupportDir(): string { $dir = dbnToolsEnv('DBN_TOOLS_SUPPORT_DIR'); if ($dir === null || trim($dir) === '') { $dir = rtrim(sys_get_temp_dir(), "\\/") . DIRECTORY_SEPARATOR . 'dbn-tools'; } if (!is_dir($dir)) { @mkdir($dir, 0770, true); } return $dir; } function dbnToolsMetadataLogPath(): string { return dbnToolsEnv('DBN_TOOLS_METADATA_LOG') ?: dbnToolsSupportDir() . DIRECTORY_SEPARATOR . 'metadata.jsonl'; } function dbnToolsLogMetadata(array $entry): void { $path = dbnToolsMetadataLogPath(); $safe = [ 'timestamp' => gmdate('c'), 'session' => dbnToolsAnonymousSessionId(), 'tool' => (string)($entry['tool'] ?? 'unknown'), 'latency_ms' => (int)($entry['latency_ms'] ?? 0), 'language' => (string)($entry['language'] ?? ''), 'ok' => (bool)($entry['ok'] ?? false), 'error_code' => $entry['error_code'] ?? null, 'chunk_count' => (int)($entry['chunk_count'] ?? 0), 'source_count' => (int)($entry['source_count'] ?? 0), 'deployment' => $entry['deployment'] ?? dbnToolsEnv('DBN_AZURE_OPENAI_CHAT_DEPLOYMENT'), ]; @file_put_contents( $path, json_encode($safe, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL, FILE_APPEND | LOCK_EX ); } function dbnToolsWithTelemetry(string $tool, string $language, callable $handler): void { $start = microtime(true); try { $payload = $handler(); $latency = (int)round((microtime(true) - $start) * 1000); $payload['ok'] = $payload['ok'] ?? true; $payload['latency_ms'] = $latency; dbnToolsLogMetadata([ 'tool' => $tool, 'language' => $language, 'ok' => true, 'latency_ms' => $latency, 'chunk_count' => (int)($payload['trace_metadata']['chunk_count'] ?? 0), 'source_count' => (int)($payload['trace_metadata']['source_count'] ?? 0), 'deployment' => $payload['trace_metadata']['deployment'] ?? null, ]); dbnToolsRespond($payload); } catch (DbnToolsHttpException $e) { $latency = (int)round((microtime(true) - $start) * 1000); dbnToolsLogMetadata([ 'tool' => $tool, 'language' => $language, 'ok' => false, 'latency_ms' => $latency, 'error_code' => $e->errorCode, ]); dbnToolsError($e->getMessage(), $e->status, $e->errorCode, $e->extra); } catch (Throwable $e) { $latency = (int)round((microtime(true) - $start) * 1000); dbnToolsLogMetadata([ 'tool' => $tool, 'language' => $language, 'ok' => false, 'latency_ms' => $latency, 'error_code' => 'internal_error', ]); error_log('DBN tools error: ' . $e->getMessage()); dbnToolsError('The tool could not complete this request.', 500, 'internal_error'); } } function dbnToolsAiPortalRoot(): string { $root = dbnToolsEnv('DBN_AI_PORTAL_ROOT'); if ($root !== null && trim($root) !== '') { return rtrim($root, "\\/"); } return dirname(DBN_TOOLS_ROOT) . DIRECTORY_SEPARATOR . 'ai-portal'; } function dbnToolsBootCaveau(): void { static $booted = false; if ($booted) { return; } $root = dbnToolsAiPortalRoot(); $dbFile = $root . DIRECTORY_SEPARATOR . 'admin' . DIRECTORY_SEPARATOR . 'includes' . DIRECTORY_SEPARATOR . 'db.php'; $ragFile = $root . DIRECTORY_SEPARATOR . 'platform' . DIRECTORY_SEPARATOR . 'includes' . DIRECTORY_SEPARATOR . 'client_rag.php'; $agentFile = $root . DIRECTORY_SEPARATOR . 'lib' . DIRECTORY_SEPARATOR . 'ai' . DIRECTORY_SEPARATOR . 'DbnLegalAgent.php'; if (!is_file($dbFile) || !is_file($ragFile)) { dbnToolsAbort('CaveauAI platform files are not available. Check DBN_AI_PORTAL_ROOT.', 503, 'caveau_unavailable'); } require_once $dbFile; require_once $ragFile; if (is_file($agentFile)) { require_once $agentFile; } $booted = true; } function dbnToolsDb(): PDO { dbnToolsBootCaveau(); try { return getDb(); } catch (Throwable $e) { throw new DbnToolsHttpException('CaveauAI database is not reachable.', 503, 'db_unavailable'); } } function dbnToolsRagDb(): PDO { dbnToolsBootCaveau(); try { return getRagDb(); } catch (Throwable $e) { throw new DbnToolsHttpException('CaveauAI corpus database is not reachable.', 503, 'rag_db_unavailable'); } } function dbnToolsClientSlug(): string { return dbnToolsEnv('DBN_CAVEAU_CLIENT_SLUG') ?: 'dave-jr-legal'; } function dbnToolsFetchClient(?PDO $db = null): ?array { $db = $db ?: dbnToolsDb(); $stmt = $db->prepare('SELECT * FROM clients WHERE slug = ? LIMIT 1'); $stmt->execute([dbnToolsClientSlug()]); $row = $stmt->fetch(PDO::FETCH_ASSOC); return $row ?: null; } function dbnToolsRequireClient(): array { $client = dbnToolsFetchClient(); if (!$client || empty($client['is_active'])) { dbnToolsAbort('Dave Jr Legal client tenant is not active or was not found.', 503, 'client_unavailable'); } return $client; } function dbnToolsFetchPackage(string $slug = 'family-legal', ?PDO $db = null): ?array { $db = $db ?: dbnToolsDb(); $stmt = $db->prepare('SELECT * FROM corpus_packages WHERE slug = ? LIMIT 1'); $stmt->execute([$slug]); $row = $stmt->fetch(PDO::FETCH_ASSOC); return $row ?: null; } function dbnToolsHasActiveSubscription(int $clientId, int $packageId, ?PDO $db = null): bool { $db = $db ?: dbnToolsDb(); $stmt = $db->prepare( 'SELECT COUNT(*) FROM client_corpus_subscriptions WHERE client_id = ? AND package_id = ? AND is_active = 1' ); $stmt->execute([$clientId, $packageId]); return (int)$stmt->fetchColumn() > 0; } function dbnToolsDisclaimer(string $language): string { if ($language === 'no') { return 'Juridisk informasjon og forberedelsesstøtte, ikke endelig juridisk rådgivning.'; } return 'Legal information and preparation support, not final legal advice.'; } function dbnToolsExcerpt(string $text, int $limit = 520): string { $text = preg_replace('/\s+/u', ' ', strip_tags($text)) ?? ''; $text = trim($text); if (mb_strlen($text, 'UTF-8') <= $limit) { return $text; } return rtrim(mb_substr($text, 0, $limit - 1, 'UTF-8')) . '…'; }