fix(app): decode imported connection link names (#1930)

Co-authored-by: zero <zero@zeroMacmini.local>
This commit is contained in:
LRcoding 2026-06-26 16:09:17 +08:00 committed by GitHub
parent 2aba40ecb7
commit f963a6e793
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 87 additions and 5 deletions

View File

@ -24,7 +24,7 @@ import DatabaseIcon from "@/components/icons/DatabaseIcon.vue";
import * as api from "@/lib/api";
import { isTauriRuntime } from "@/lib/tauriRuntime";
import { applyParsedConnectionUrl, normalizeMongoConnectionString, parseConnectionUrl } from "@/lib/connectionUrl";
import type { ConnectionDeepLinkDraft } from "@/lib/connectionDeepLink";
import { parseConnectionDeepLink, type ConnectionDeepLinkDraft } from "@/lib/connectionDeepLink";
import { connectionUrlPlaceholder as getUrlPlaceholder } from "@/lib/connectionPresentation";
import { h2ConnectionModeForConfig, h2FileJdbcUrl, h2FilePathFromJdbcUrl, type H2ConnectionMode } from "@/lib/h2Connection";
import { firstZooKeeperEndpoint, normalizeZooKeeperConnectString } from "@/lib/zookeeperConnection";
@ -1464,6 +1464,12 @@ async function testConnection() {
function applyConnectionUrlToForm(input: string): boolean {
try {
const draft = parseConnectionDeepLink(input);
if (draft) {
applyConnectionDraftToForm({ ...draft, oneTime: undefined });
return true;
}
const parsed = parseConnectionUrl(input, selectedType.value);
form.value = applyParsedConnectionUrl(form.value, parsed);
selectedType.value = parsed.driverProfile;
@ -2146,8 +2152,7 @@ function submitOneTimePrefill(draft: ConnectionDeepLinkDraft) {
void nextTick(() => save());
}
function applyConnectionPrefill(draft: ConnectionDeepLinkDraft) {
resetForm();
function applyConnectionDraftToForm(draft: ConnectionDeepLinkDraft) {
applyProfile(draft.driverProfile);
form.value = {
...form.value,
@ -2183,6 +2188,11 @@ function applyConnectionPrefill(draft: ConnectionDeepLinkDraft) {
dialogStep.value = "config";
configTab.value = "connection";
resetTestState();
}
function applyConnectionPrefill(draft: ConnectionDeepLinkDraft) {
resetForm();
applyConnectionDraftToForm(draft);
submitOneTimePrefill(draft);
}

View File

@ -48,6 +48,7 @@ function optionalBooleanParam(params: URLSearchParams, ...keys: string[]): boole
function draftFromConnectionUrl(value: string, preferredProfile?: string): ConnectionDeepLinkDraft {
const parsed = parseConnectionUrl(value, preferredProfile);
return {
name: parsed.name,
dbType: parsed.dbType,
driverProfile: parsed.driverProfile,
driverLabel: parsed.driverLabel,

View File

@ -1,6 +1,7 @@
import type { ConnectionConfig, DatabaseType } from "@/types/database";
export interface ParsedConnectionUrl {
name?: string;
dbType: DatabaseType;
driverProfile: string;
driverLabel: string;
@ -172,6 +173,28 @@ function queryParamValue(params: string, key: string): string | undefined {
return undefined;
}
function connectionNameParam(parsed: URL): string | undefined {
for (const [key, value] of parsed.searchParams) {
if (key.toLowerCase() === "name") {
const name = value.trim();
if (name) return name;
}
}
return undefined;
}
function stripConnectionNameParam(params: string): string {
if (!params) return params;
return params
.split("&")
.filter((part) => {
if (!part) return true;
const [rawKey] = part.split("=");
return decodeUrlPart(rawKey).trim().toLowerCase() !== "name";
})
.join("&");
}
function extractMysqlCredentialParams(params: string): { username?: string; password?: string; urlParams: string } {
let username: string | undefined;
let password: string | undefined;
@ -430,8 +453,10 @@ export function parseConnectionUrl(value: string, preferredProfile?: string): Pa
}
const urlParams = parsed.search.replace(/^\?/, "");
const name = connectionNameParam(parsed);
const urlParamsWithoutName = stripConnectionNameParam(urlParams);
const normalizedFragment = decodeUrlPart(parsed.hash.replace(/^#/, "")).trim().toLowerCase();
const parsedUrlParams = profile.type === "redis" && normalizedFragment === "insecure" ? [urlParams, "insecure=true"].filter(Boolean).join("&") : urlParams;
const parsedUrlParams = profile.type === "redis" && normalizedFragment === "insecure" ? [urlParamsWithoutName, "insecure=true"].filter(Boolean).join("&") : urlParamsWithoutName;
const mysqlCredentials = isJdbcUrl && profile.type === "mysql" ? extractMysqlCredentialParams(parsedUrlParams) : undefined;
const effectiveUrlParams = mysqlCredentials?.urlParams ?? parsedUrlParams;
if (profile.type === "mongodb") {
@ -452,6 +477,7 @@ export function parseConnectionUrl(value: string, preferredProfile?: string): Pa
}
if (profile.type === "zookeeper") {
return {
...(name ? { name } : {}),
dbType: profile.type,
driverProfile: profile.profile,
driverLabel: profile.label,
@ -460,13 +486,14 @@ export function parseConnectionUrl(value: string, preferredProfile?: string): Pa
username: decodeUrlPart(parsed.username),
password: decodeUrlPart(parsed.password),
database: undefined,
urlParams,
urlParams: urlParamsWithoutName,
ssl: false,
connectionString: zookeeperConnectStringFromUrl(parsed, profile.defaultPort),
};
}
return {
...(name ? { name } : {}),
dbType: profile.type,
driverProfile: profile.profile,
driverLabel: profile.label,
@ -496,6 +523,7 @@ export function applyParsedConnectionUrl(config: Omit<ConnectionConfig, "id">, p
driver_label: parsed.driverLabel,
host: parsed.host,
port: parsed.port,
name: parsed.name?.trim() || config.name,
username: parsed.username,
password: parsed.password,
database: parsed.database,

View File

@ -33,6 +33,23 @@ test("parses encoded database URL with password", () => {
assert.equal(draft?.urlParams, "sslmode=require");
});
test("uses the nested database URL name as connection name", () => {
const nested = encodeURIComponent("mysql://root:123456@localhost/?name=%E5%85%AC%E5%8F%B8+-+%E6%9C%AC%E5%9C%B0Docker&charset=utf8mb4");
const draft = parseConnectionDeepLink(`dbx://connection/new?url=${nested}`);
assert.equal(draft?.name, "公司 - 本地Docker");
assert.equal(draft?.dbType, "mysql");
assert.equal(draft?.host, "localhost");
assert.equal(draft?.urlParams, "charset=utf8mb4");
});
test("top-level deep link name overrides nested database URL name", () => {
const nested = encodeURIComponent("mysql://root@localhost/?name=Nested");
const draft = parseConnectionDeepLink(`dbx://connection/new?url=${nested}&name=Top+Level`);
assert.equal(draft?.name, "Top Level");
});
test("allows password query field to override database URL password", () => {
const draft = parseConnectionDeepLink("dbx://connection/new?url=postgres%3A%2F%2Fapp%3Asecret%40db.internal%3A5432%2Forders&password=override");

View File

@ -45,6 +45,32 @@ test("parses mysql URLs with encoded credentials", () => {
assert.equal(parsed.urlParams, "charset=utf8mb4");
});
test("parses mysql URL name as decoded connection name", () => {
const parsed = parseConnectionUrl("mysql://root:123456@localhost/?name=%E5%85%AC%E5%8F%B8+-+%E6%9C%AC%E5%9C%B0Docker&charset=utf8mb4");
assert.equal(parsed.name, "公司 - 本地Docker");
assert.equal(parsed.host, "localhost");
assert.equal(parsed.username, "root");
assert.equal(parsed.password, "123456");
assert.equal(parsed.urlParams, "charset=utf8mb4");
});
test("consumes mysql URL name when it is the only URL param", () => {
const parsed = parseConnectionUrl("mysql://root:123456@localhost/?name=%E5%85%AC%E5%8F%B8+-+%E6%9C%AC%E5%9C%B0Docker");
assert.equal(parsed.name, "公司 - 本地Docker");
assert.equal(parsed.urlParams, "");
});
test("removes only the connection name from URL params", () => {
const parsed = parseConnectionUrl("mysql://root@localhost/app?Name=Analytics+Local&ssl-mode=required");
assert.equal(parsed.name, "Analytics Local");
assert.equal(parsed.database, "app");
assert.equal(parsed.urlParams, "ssl-mode=required");
assert.equal(parsed.ssl, true);
});
test("parses mysql TLS URL params into the SSL switch state", () => {
assert.equal(parseConnectionUrl("mysql://root@tidb.example.com:4000/test?ssl-mode=required").ssl, true);
assert.equal(parseConnectionUrl("mysql://root@tidb.example.com:4000/test?require_ssl=true").ssl, true);