Skip to content

Commit 8a3cd38

Browse files
committed
fix: download dashboard subscriptions same-origin
1 parent 522a592 commit 8a3cd38

2 files changed

Lines changed: 44 additions & 2 deletions

File tree

packages/ui/src/dashboard/subscription-dashboard-surface.test.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,12 @@ describe("SubscriptionDashboardSurface", () => {
239239
getItem: vi.fn(() => null),
240240
setItem: vi.fn(),
241241
});
242+
vi.stubGlobal("window", {
243+
location: {
244+
href: "http://localhost/dashboard",
245+
origin: "http://localhost",
246+
},
247+
});
242248
});
243249

244250
afterEach(() => {
@@ -344,8 +350,11 @@ describe("SubscriptionDashboardSurface", () => {
344350
const fetchMock = vi.fn(async () => ({ ok: true, status: 200, blob: vi.fn(async () => blob) }));
345351
const createObjectURL = vi.fn(() => "blob:subboost-config");
346352
const revokeObjectURL = vi.fn();
353+
class TestURL extends URL {}
354+
TestURL.createObjectURL = createObjectURL;
355+
TestURL.revokeObjectURL = revokeObjectURL;
347356
vi.stubGlobal("fetch", fetchMock);
348-
vi.stubGlobal("URL", { createObjectURL, revokeObjectURL });
357+
vi.stubGlobal("URL", TestURL);
349358

350359
renderSurface(createAdapter(), { 0: [subscription], 1: false, 2: null, 3: null });
351360
await mocks.captures.buttons.find((props: any) => props.title === "下载订阅配置").onClick();
@@ -379,6 +388,29 @@ describe("SubscriptionDashboardSurface", () => {
379388
}));
380389
});
381390

391+
it("fetches cross-origin subscription links through the current origin for downloads", async () => {
392+
const dom = stubDocumentActions();
393+
const blob = new Blob(["mixed-port: 7890\n"], { type: "text/yaml" });
394+
const fetchMock = vi.fn(async () => ({ ok: true, status: 200, blob: vi.fn(async () => blob) }));
395+
vi.stubGlobal("fetch", fetchMock);
396+
class TestURL extends URL {}
397+
TestURL.createObjectURL = vi.fn(() => "blob:subboost-config");
398+
TestURL.revokeObjectURL = vi.fn();
399+
vi.stubGlobal("URL", TestURL);
400+
401+
renderSurface(createAdapter(), {
402+
0: [{ ...subscription, subscriptionUrl: "https://ryan-ai.de/api/subscription/token-1?download=1" }],
403+
1: false,
404+
2: null,
405+
3: null,
406+
});
407+
await mocks.captures.buttons.find((props: any) => props.title === "下载订阅配置").onClick();
408+
await flushPromises();
409+
410+
expect(fetchMock).toHaveBeenCalledWith("http://localhost/api/subscription/token-1?download=1");
411+
expect(dom.anchor.download).toBe("Primary.yaml");
412+
});
413+
382414
it("guards cancelled delete and in-flight refresh failures", async () => {
383415
const adapter = createAdapter({ refreshSubscription: vi.fn(async () => { throw new Error("refresh failed"); }) });
384416
renderSurface(adapter, { 0: [subscription], 1: false, 2: null, 3: "sub-1" });

packages/ui/src/dashboard/subscription-dashboard-surface.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,16 @@ function triggerBrowserDownload(href: string, filename: string) {
8888
anchor.remove();
8989
}
9090

91+
function buildSameOriginDownloadUrl(subscriptionUrl: string): string {
92+
try {
93+
const url = new URL(subscriptionUrl, window.location.href);
94+
if (!url.pathname.includes("/api/subscription")) return subscriptionUrl;
95+
return `${window.location.origin}${url.pathname}${url.search}`;
96+
} catch {
97+
return subscriptionUrl;
98+
}
99+
}
100+
91101
async function copyText(text: string): Promise<boolean> {
92102
try {
93103
if (navigator.clipboard?.writeText) {
@@ -207,7 +217,7 @@ export function SubscriptionDashboardSurface({ adapter }: Props) {
207217
const downloadSubscription = async (subscription: Subscription) => {
208218
const filename = buildYamlDownloadFilename(subscription.name);
209219
try {
210-
const response = await fetch(subscription.subscriptionUrl);
220+
const response = await fetch(buildSameOriginDownloadUrl(subscription.subscriptionUrl));
211221
if (!response.ok) throw new Error(`Download failed with status ${response.status}`);
212222
const blob = await response.blob();
213223
const objectUrl = URL.createObjectURL(blob);

0 commit comments

Comments
 (0)