diff --git a/web/app/components/integrations/page.tsx b/web/app/components/integrations/page.tsx index ce07fb0586e..25ca6923004 100644 --- a/web/app/components/integrations/page.tsx +++ b/web/app/components/integrations/page.tsx @@ -162,7 +162,7 @@ export default function IntegrationsPage({ return } - window.open(getMarketplaceUrl(marketplaceUrlPath), '_blank', 'noopener,noreferrer') + window.open(getMarketplaceUrl(marketplaceUrlPath, undefined, { source: window.location.origin }), '_blank', 'noopener,noreferrer') } const handleSelectSection = (nextSection: IntegrationSection) => { if (onSectionChange) { diff --git a/web/app/components/tools/integrations-setting-modal.tsx b/web/app/components/tools/integrations-setting-modal.tsx index 7bd2cee75e3..d5cd7984153 100644 --- a/web/app/components/tools/integrations-setting-modal.tsx +++ b/web/app/components/tools/integrations-setting-modal.tsx @@ -25,7 +25,7 @@ export default function IntegrationsSettingModal({ const { t } = useTranslation() const isAgentSource = source === 'agent' const handleSwitchToMarketplace = useCallback((path: string) => { - window.open(getMarketplaceUrl(path), '_blank', 'noopener,noreferrer') + window.open(getMarketplaceUrl(path, undefined, { source: window.location.origin }), '_blank', 'noopener,noreferrer') }, []) return ( diff --git a/web/utils/var.spec.ts b/web/utils/var.spec.ts index 9120b7a7841..d5ff8b0687e 100644 --- a/web/utils/var.spec.ts +++ b/web/utils/var.spec.ts @@ -1,3 +1,4 @@ +import { describe, expect, it, vi } from 'vitest' import { InputVarType } from '@/app/components/workflow/types' import { checkKey, @@ -207,6 +208,30 @@ describe('Variable Utilities', () => { expect(url).toContain('category=ai') expect(url).not.toContain('version=') }) + + it('should include provided source without double encoding', () => { + const url = getMarketplaceUrl('/plugins', { category: 'ai' }, { source: 'https://example.com/app' }) + expect(url).toContain('source=https%3A%2F%2Fexample.com') + expect(url).not.toContain('source=https%253A%252F%252Fexample.com') + }) + + it('should not access window during server render', () => { + const originalWindow = window + vi.stubGlobal('window', undefined) + + try { + const url = getMarketplaceUrl('/plugins', { category: 'ai' }) + expect(url).toContain('category=ai') + expect(url).not.toContain('source=') + } + finally { + vi.stubGlobal('window', originalWindow) + } + }) + + it('should not append empty query string when no params are available', () => { + expect(getMarketplaceUrl('/plugins')).toBe('/plugins') + }) }) describe('replaceSpaceWithUnderscoreInVarNameInput', () => { diff --git a/web/utils/var.ts b/web/utils/var.ts index 8c06c7e8c90..b1fdd775790 100644 --- a/web/utils/var.ts +++ b/web/utils/var.ts @@ -132,8 +132,30 @@ export const getVars = (value: string) => { // example: /dify export const basePath = env.NEXT_PUBLIC_BASE_PATH -export function getMarketplaceUrl(path: string, params?: Record) { - const searchParams = new URLSearchParams({ source: encodeURIComponent(window.location.origin) }) +type MarketplaceUrlOptions = { + source?: string +} + +const getUrlOrigin = (url?: string) => { + if (!url) + return undefined + + try { + return new URL(url).origin + } + catch { + return undefined + } +} + +const marketplaceSource = getUrlOrigin(env.NEXT_PUBLIC_WEB_PREFIX) + +export function getMarketplaceUrl(path: string, params?: Record, options?: MarketplaceUrlOptions) { + const searchParams = new URLSearchParams() + const source = getUrlOrigin(options?.source) ?? marketplaceSource + if (source) + searchParams.set('source', source) + if (params) { Object.keys(params).forEach((key) => { const value = params[key] @@ -141,7 +163,9 @@ export function getMarketplaceUrl(path: string, params?: Record {