diff --git a/src/components/dialog/content/MissingModelsWarning.vue b/src/components/dialog/content/MissingModelsWarning.vue index df1c7d470..376ea17b9 100644 --- a/src/components/dialog/content/MissingModelsWarning.vue +++ b/src/components/dialog/content/MissingModelsWarning.vue @@ -38,23 +38,14 @@ import { useI18n } from 'vue-i18n' import FileDownload from '@/components/common/FileDownload.vue' import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue' +import { MODEL_SOURCES } from '@/constants/urls' import { useSettingStore } from '@/stores/settingStore' import { isElectron } from '@/utils/envUtil' -// TODO: Read this from server internal API rather than hardcoding here -// as some installations may wish to use custom sources -const allowedSources = [ - 'https://civitai.com/', - 'https://huggingface.co/', - 'http://localhost:' // Included for testing usage only -] +/** @todo Read this from server internal API rather than hardcoding here */ +const allowedSources = MODEL_SOURCES.allowedDomains const allowedSuffixes = ['.safetensors', '.sft'] -// Models that fail above conditions but are still allowed -const whiteListedUrls = new Set([ - 'https://huggingface.co/stabilityai/stable-zero123/resolve/main/stable_zero123.ckpt', - 'https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_depth_sd14v1.pth?download=true', - 'https://github.com/xinntao/Real-ESRGAN/releases/download/v0.1.0/RealESRGAN_x4plus.pth' -]) +const whiteListedUrls = new Set(MODEL_SOURCES.whitelistedUrls) interface ModelInfo { name: string diff --git a/src/components/helpcenter/HelpCenterMenuContent.vue b/src/components/helpcenter/HelpCenterMenuContent.vue index 45a6da119..519060c02 100644 --- a/src/components/helpcenter/HelpCenterMenuContent.vue +++ b/src/components/helpcenter/HelpCenterMenuContent.vue @@ -123,6 +123,7 @@ import Button from 'primevue/button' import { type CSSProperties, computed, nextTick, onMounted, ref } from 'vue' import { useI18n } from 'vue-i18n' +import { COMFY_URLS, GITHUB_REPOS, getDesktopGuideUrl } from '@/constants/urls' import { type ReleaseNote } from '@/services/releaseService' import { useCommandStore } from '@/stores/commandStore' import { useReleaseStore } from '@/stores/releaseStore' @@ -143,11 +144,10 @@ interface MenuItem { // Constants const EXTERNAL_LINKS = { - DOCS: 'https://docs.comfy.org/', - DISCORD: 'https://www.comfy.org/discord', - GITHUB: 'https://github.com/comfyanonymous/ComfyUI', - DESKTOP_GUIDE: 'https://comfyorg.notion.site/', - UPDATE_GUIDE: 'https://docs.comfy.org/installation/update_comfyui' + DOCS: COMFY_URLS.docs.base, + DISCORD: COMFY_URLS.community.discord, + GITHUB: GITHUB_REPOS.comfyui, + UPDATE_GUIDE: COMFY_URLS.docs.installation.update } as const const TIME_UNITS = { @@ -199,7 +199,7 @@ const menuItems = computed(() => { type: 'item', label: t('helpCenter.desktopUserGuide'), action: () => { - openExternalLink(EXTERNAL_LINKS.DESKTOP_GUIDE) + openExternalLink(getDesktopGuideUrl(locale.value)) emit('close') } }, @@ -451,8 +451,8 @@ const onUpdate = (_: ReleaseNote): void => { const getChangelogUrl = (): string => { const isChineseLocale = locale.value === 'zh' return isChineseLocale - ? 'https://docs.comfy.org/zh-CN/changelog' - : 'https://docs.comfy.org/changelog' + ? COMFY_URLS.docs.changelog.zh + : COMFY_URLS.docs.changelog.en } // Lifecycle diff --git a/src/components/helpcenter/ReleaseNotificationToast.vue b/src/components/helpcenter/ReleaseNotificationToast.vue index 4984b84d8..23ffd4fef 100644 --- a/src/components/helpcenter/ReleaseNotificationToast.vue +++ b/src/components/helpcenter/ReleaseNotificationToast.vue @@ -48,6 +48,7 @@ import { computed, onMounted, ref, watch } from 'vue' import { useI18n } from 'vue-i18n' +import { COMFY_URLS } from '@/constants/urls' import type { ReleaseNote } from '@/services/releaseService' import { useReleaseStore } from '@/stores/releaseStore' import { formatVersionAnchor } from '@/utils/formatUtil' @@ -72,8 +73,8 @@ const shouldShow = computed( const changelogUrl = computed(() => { const isChineseLocale = locale.value === 'zh' const baseUrl = isChineseLocale - ? 'https://docs.comfy.org/zh-CN/changelog' - : 'https://docs.comfy.org/changelog' + ? COMFY_URLS.docs.changelog.zh + : COMFY_URLS.docs.changelog.en if (latestRelease.value?.version) { const versionAnchor = formatVersionAnchor(latestRelease.value.version) @@ -120,7 +121,7 @@ const handleLearnMore = () => { } const handleUpdate = () => { - window.open('https://docs.comfy.org/installation/update_comfyui', '_blank') + window.open(COMFY_URLS.docs.installation.update, '_blank') dismissToast() } diff --git a/src/components/helpcenter/WhatsNewPopup.vue b/src/components/helpcenter/WhatsNewPopup.vue index 26d1dc8a0..b8f68c287 100644 --- a/src/components/helpcenter/WhatsNewPopup.vue +++ b/src/components/helpcenter/WhatsNewPopup.vue @@ -68,6 +68,7 @@ import { marked } from 'marked' import { computed, onMounted, ref } from 'vue' import { useI18n } from 'vue-i18n' +import { COMFY_URLS } from '@/constants/urls' import type { ReleaseNote } from '@/services/releaseService' import { useReleaseStore } from '@/stores/releaseStore' import { formatVersionAnchor } from '@/utils/formatUtil' @@ -92,8 +93,8 @@ const shouldShow = computed( const changelogUrl = computed(() => { const isChineseLocale = locale.value === 'zh' const baseUrl = isChineseLocale - ? 'https://docs.comfy.org/zh-CN/changelog' - : 'https://docs.comfy.org/changelog' + ? COMFY_URLS.docs.changelog.zh + : COMFY_URLS.docs.changelog.en if (latestRelease.value?.version) { const versionAnchor = formatVersionAnchor(latestRelease.value.version) diff --git a/src/components/install/MirrorsConfiguration.vue b/src/components/install/MirrorsConfiguration.vue index 0053f973a..b8c8028ab 100644 --- a/src/components/install/MirrorsConfiguration.vue +++ b/src/components/install/MirrorsConfiguration.vue @@ -43,7 +43,7 @@ import Panel from 'primevue/panel' import { ModelRef, computed, onMounted, ref } from 'vue' import MirrorItem from '@/components/install/mirror/MirrorItem.vue' -import { PYPI_MIRROR, PYTHON_MIRROR, UVMirror } from '@/constants/uvMirrors' +import { PYPI_MIRROR, PYTHON_MIRROR, UVMirror } from '@/constants/mirrors' import { t } from '@/i18n' import { isInChina } from '@/utils/networkUtil' import { ValidationState, mergeValidationStates } from '@/utils/validationUtil' diff --git a/src/components/install/mirror/MirrorItem.vue b/src/components/install/mirror/MirrorItem.vue index 3bd83074e..cb07a4b3f 100644 --- a/src/components/install/mirror/MirrorItem.vue +++ b/src/components/install/mirror/MirrorItem.vue @@ -23,7 +23,7 @@ import { computed, onMounted, ref, watch } from 'vue' import UrlInput from '@/components/common/UrlInput.vue' -import { UVMirror } from '@/constants/uvMirrors' +import { UVMirror } from '@/constants/mirrors' import { normalizeI18nKey } from '@/utils/formatUtil' import { checkMirrorReachable } from '@/utils/networkUtil' import { ValidationState } from '@/utils/validationUtil' diff --git a/src/composables/useCoreCommands.ts b/src/composables/useCoreCommands.ts index 5e4cf6125..997bf59dd 100644 --- a/src/composables/useCoreCommands.ts +++ b/src/composables/useCoreCommands.ts @@ -11,6 +11,7 @@ import { DEFAULT_DARK_COLOR_PALETTE, DEFAULT_LIGHT_COLOR_PALETTE } from '@/constants/coreColorPalettes' +import { COMFY_URLS, GITHUB_REPOS } from '@/constants/urls' import { t } from '@/i18n' import { api } from '@/scripts/api' import { app } from '@/scripts/app' @@ -543,10 +544,7 @@ export function useCoreCommands(): ComfyCommand[] { menubarLabel: 'ComfyUI Issues', versionAdded: '1.5.5', function: () => { - window.open( - 'https://github.com/comfyanonymous/ComfyUI/issues', - '_blank' - ) + window.open(GITHUB_REPOS.comfyuiIssues, '_blank') } }, { @@ -556,7 +554,7 @@ export function useCoreCommands(): ComfyCommand[] { menubarLabel: 'ComfyUI Docs', versionAdded: '1.5.5', function: () => { - window.open('https://docs.comfy.org/', '_blank') + window.open(COMFY_URLS.docs.base, '_blank') } }, { @@ -566,7 +564,7 @@ export function useCoreCommands(): ComfyCommand[] { menubarLabel: 'Comfy-Org Discord', versionAdded: '1.5.5', function: () => { - window.open('https://www.comfy.org/discord', '_blank') + window.open(COMFY_URLS.community.discord, '_blank') } }, { @@ -646,7 +644,7 @@ export function useCoreCommands(): ComfyCommand[] { menubarLabel: 'ComfyUI Forum', versionAdded: '1.8.2', function: () => { - window.open('https://forum.comfy.org/', '_blank') + window.open(COMFY_URLS.community.forum.base, '_blank') } }, { diff --git a/src/config/comfyDomain.ts b/src/config/comfyDomain.ts new file mode 100644 index 000000000..0afc388a3 --- /dev/null +++ b/src/config/comfyDomain.ts @@ -0,0 +1,14 @@ +/** + * Base domain configuration and core website URLs + * Forkers can change the base domain to use their own + */ +export const COMFY_BASE_DOMAIN = + process.env.VITE_COMFY_BASE_DOMAIN || 'comfy.org' + +const WEBSITE_BASE_URL = `https://www.${COMFY_BASE_DOMAIN}` + +export const COMFY_WEBSITE_URLS = { + base: WEBSITE_BASE_URL, + termsOfService: `${WEBSITE_BASE_URL}/terms-of-service`, + privacy: `${WEBSITE_BASE_URL}/privacy` +} diff --git a/src/constants/uvMirrors.ts b/src/constants/mirrors.ts similarity index 100% rename from src/constants/uvMirrors.ts rename to src/constants/mirrors.ts diff --git a/src/constants/urls.ts b/src/constants/urls.ts new file mode 100644 index 000000000..20f6a2e46 --- /dev/null +++ b/src/constants/urls.ts @@ -0,0 +1,95 @@ +/** + * URL constants for ComfyUI frontend + * Centralized location for all URL references + */ +import { COMFY_BASE_DOMAIN } from '@/config/comfyDomain' + +const DOCS_BASE_URL = `https://docs.${COMFY_BASE_DOMAIN}` +const FORUM_BASE_URL = `https://forum.${COMFY_BASE_DOMAIN}` +const WEBSITE_BASE_URL = `https://www.${COMFY_BASE_DOMAIN}` + +export const COMFY_URLS = { + website: { + base: WEBSITE_BASE_URL, + termsOfService: `${WEBSITE_BASE_URL}/terms-of-service`, + privacy: `${WEBSITE_BASE_URL}/privacy` + }, + docs: { + base: DOCS_BASE_URL, + changelog: { + en: `${DOCS_BASE_URL}/changelog`, + zh: `${DOCS_BASE_URL}/zh-CN/changelog` + }, + installation: { + update: `${DOCS_BASE_URL}/installation/update_comfyui` + }, + tutorials: { + apiNodes: { + overview: `${DOCS_BASE_URL}/tutorials/api-nodes/overview`, + faq: `${DOCS_BASE_URL}/tutorials/api-nodes/faq`, + pricing: `${DOCS_BASE_URL}/tutorials/api-nodes/pricing`, + apiKeyLogin: `${DOCS_BASE_URL}/interface/user#logging-in-with-an-api-key`, + nonWhitelistedLogin: `${DOCS_BASE_URL}/tutorials/api-nodes/overview#log-in-with-api-key-on-non-whitelisted-websites` + } + }, + getLocalized: (path: string, locale: string) => { + return locale === 'zh' || locale === 'zh-CN' + ? `${DOCS_BASE_URL}/zh-CN/${path}` + : `${DOCS_BASE_URL}/${path}` + } + }, + community: { + discord: `${WEBSITE_BASE_URL}/discord`, + forum: { + base: `${FORUM_BASE_URL}/`, + v1Feedback: `${FORUM_BASE_URL}/c/v1-feedback/` + } + } +} + +export const GITHUB_REPOS = { + comfyui: 'https://github.com/comfyanonymous/ComfyUI', + comfyuiIssues: 'https://github.com/comfyanonymous/ComfyUI/issues', + frontend: 'https://github.com/Comfy-Org/ComfyUI_frontend', + electron: 'https://github.com/Comfy-Org/electron', + desktopPlatforms: + 'https://github.com/Comfy-Org/desktop#currently-supported-platforms' +} + +export const MODEL_SOURCES = { + repos: { + civitai: 'https://civitai.com/', + huggingface: 'https://huggingface.co/' + }, + whitelistedUrls: [ + 'https://huggingface.co/stabilityai/stable-zero123/resolve/main/stable_zero123.ckpt', + 'https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_depth_sd14v1.pth?download=true', + 'https://github.com/xinntao/Real-ESRGAN/releases/download/v0.1.0/RealESRGAN_x4plus.pth' + ], + allowedDomains: [ + 'https://civitai.com/', + 'https://huggingface.co/', + 'http://localhost:' // TODO: Remove in production + ] +} + +export const DEVELOPER_TOOLS = { + git: 'https://git-scm.com/downloads/', + vcRedist: 'https://aka.ms/vs/17/release/vc_redist.x64.exe', + uv: 'https://docs.astral.sh/uv/getting-started/installation/' +} + +// Platform and locale-aware desktop guide URL generator +export const getDesktopGuideUrl = (locale: string): string => { + const isChineseLocale = locale === 'zh' + const isMacOS = + typeof navigator !== 'undefined' && + navigator.platform.toUpperCase().indexOf('MAC') >= 0 + + const platform = isMacOS ? 'macos' : 'windows' + const baseUrl = isChineseLocale + ? `https://docs.${COMFY_BASE_DOMAIN}/zh-CN/installation/desktop` + : `https://docs.${COMFY_BASE_DOMAIN}/installation/desktop` + + return `${baseUrl}/${platform}` +} diff --git a/src/extensions/core/electronAdapter.ts b/src/extensions/core/electronAdapter.ts index da28ac0c2..245310de7 100644 --- a/src/extensions/core/electronAdapter.ts +++ b/src/extensions/core/electronAdapter.ts @@ -1,7 +1,8 @@ import log from 'loglevel' -import { PYTHON_MIRROR } from '@/constants/uvMirrors' -import { t } from '@/i18n' +import { PYTHON_MIRROR } from '@/constants/mirrors' +import { getDesktopGuideUrl } from '@/constants/urls' +import { i18n, t } from '@/i18n' import { app } from '@/scripts/app' import { useDialogService } from '@/services/dialogService' import { useToastStore } from '@/stores/toastStore' @@ -159,7 +160,7 @@ import { checkMirrorReachable } from '@/utils/networkUtil' label: 'Desktop User Guide', icon: 'pi pi-book', function() { - window.open('https://comfyorg.notion.site/', '_blank') + window.open(getDesktopGuideUrl(i18n.global.locale.value), '_blank') } }, { diff --git a/src/stores/aboutPanelStore.ts b/src/stores/aboutPanelStore.ts index cdb626095..399aa6bb8 100644 --- a/src/stores/aboutPanelStore.ts +++ b/src/stores/aboutPanelStore.ts @@ -1,6 +1,7 @@ import { defineStore } from 'pinia' import { computed } from 'vue' +import { COMFY_URLS, GITHUB_REPOS } from '@/constants/urls' import { AboutPageBadge } from '@/types/comfy' import { electronAPI, isElectron } from '@/utils/envUtil' @@ -24,20 +25,20 @@ export const useAboutPanelStore = defineStore('aboutPanel', () => { ? 'v' + electronAPI().getComfyUIVersion() : coreVersion.value }`, - url: 'https://github.com/comfyanonymous/ComfyUI', + url: GITHUB_REPOS.comfyui, icon: 'pi pi-github' }, { label: `ComfyUI_frontend v${frontendVersion}`, - url: 'https://github.com/Comfy-Org/ComfyUI_frontend', + url: GITHUB_REPOS.frontend, icon: 'pi pi-github' }, { label: 'Discord', - url: 'https://www.comfy.org/discord', + url: COMFY_URLS.community.discord, icon: 'pi pi-discord' }, - { label: 'ComfyOrg', url: 'https://www.comfy.org/', icon: 'pi pi-globe' } + { label: 'ComfyOrg', url: COMFY_URLS.website.base, icon: 'pi pi-globe' } ]) const allBadges = computed(() => [ diff --git a/tests-ui/tests/constants/urlConstants.test.ts b/tests-ui/tests/constants/urlConstants.test.ts new file mode 100644 index 000000000..eb99ed3c7 --- /dev/null +++ b/tests-ui/tests/constants/urlConstants.test.ts @@ -0,0 +1,144 @@ +import { describe, expect, it } from 'vitest' + +import { COMFY_BASE_DOMAIN, COMFY_WEBSITE_URLS } from '@/config/comfyDomain' +import { PYPI_MIRROR, PYTHON_MIRROR } from '@/constants/mirrors' +import { + COMFY_URLS, + DEVELOPER_TOOLS, + GITHUB_REPOS, + MODEL_SOURCES, + getDesktopGuideUrl +} from '@/constants/urls' + +describe('URL Constants', () => { + describe('URL Format Validation', () => { + it('should have valid HTTPS URLs throughout', () => { + const httpsPattern = + /^https:\/\/[a-z0-9]+([-.][a-z0-9]+)*\.[a-z]{2,}(:[0-9]{1,5})?(\/.*)?$/i + + // Test COMFY_URLS + expect(COMFY_URLS.website.base).toMatch(httpsPattern) + expect(COMFY_URLS.docs.base).toMatch(httpsPattern) + expect(COMFY_URLS.community.discord).toMatch(httpsPattern) + + // Test GITHUB_REPOS + Object.values(GITHUB_REPOS).forEach((url) => { + expect(url).toMatch(httpsPattern) + }) + + // Test MODEL_SOURCES + Object.values(MODEL_SOURCES.repos).forEach((url) => { + expect(url).toMatch(httpsPattern) + }) + + // Test DEVELOPER_TOOLS + Object.values(DEVELOPER_TOOLS).forEach((url) => { + expect(url).toMatch(httpsPattern) + }) + }) + + it('should have proper GitHub URL format', () => { + const githubPattern = /^https:\/\/github\.com\/[\w-]+\/[\w-]+(\/[\w-]+)?$/ + + expect(GITHUB_REPOS.comfyui).toMatch(githubPattern) + expect(GITHUB_REPOS.comfyuiIssues).toMatch(githubPattern) + expect(GITHUB_REPOS.frontend).toMatch(githubPattern) + }) + }) + + describe('Mirror Configuration', () => { + it('should have valid mirror URLs', () => { + const urlPattern = + /^https?:\/\/[a-z0-9]+([-\.]{1}[a-z0-9]+)*\.[a-z]{2,}(:[0-9]{1,5})?(\/.*)?$/i + + expect(PYTHON_MIRROR.mirror).toMatch(urlPattern) + expect(PYTHON_MIRROR.fallbackMirror).toMatch(urlPattern) + expect(PYPI_MIRROR.mirror).toMatch(urlPattern) + expect(PYPI_MIRROR.fallbackMirror).toMatch(urlPattern) + }) + }) + + describe('Domain Configuration', () => { + it('should have valid domain format', () => { + const domainPattern = /^[a-z0-9]+([-.][a-z0-9]+)*\.[a-z]{2,}$/i + expect(COMFY_BASE_DOMAIN).toMatch(domainPattern) + }) + + it('should construct proper website URLs from base domain', () => { + const expectedBase = `https://www.${COMFY_BASE_DOMAIN}` + expect(COMFY_WEBSITE_URLS.base).toBe(expectedBase) + expect(COMFY_WEBSITE_URLS.termsOfService).toBe( + `${expectedBase}/terms-of-service` + ) + expect(COMFY_WEBSITE_URLS.privacy).toBe(`${expectedBase}/privacy`) + }) + }) + + describe('Localization', () => { + it('should handle valid language codes in getLocalized', () => { + const validLanguageCodes = ['en', 'zh', 'ja', 'ko', 'es', 'fr', 'de'] + + validLanguageCodes.forEach((lang) => { + const result = COMFY_URLS.docs.getLocalized('test-path', lang) + expect(result).toMatch(/^\/docs\/(([a-z]{2}-[A-Z]{2}\/)?test-path)$/) + }) + }) + + it('should properly format localized paths', () => { + expect(COMFY_URLS.docs.getLocalized('test-path', 'en')).toBe( + '/docs/test-path' + ) + expect(COMFY_URLS.docs.getLocalized('test-path', 'zh')).toBe( + '/docs/zh-CN/test-path' + ) + expect(COMFY_URLS.docs.getLocalized('/', 'en')).toBe('/docs/') + expect(COMFY_URLS.docs.getLocalized('/', 'zh')).toBe('/docs/zh-CN/') + }) + + it('should generate platform and locale-aware desktop guide URLs', () => { + // Test English locale + const enUrl = getDesktopGuideUrl('en') + expect(enUrl).toMatch( + /^https:\/\/docs\.comfy\.org\/installation\/desktop\/(windows|macos)$/ + ) + + // Test Chinese locale + const zhUrl = getDesktopGuideUrl('zh') + expect(zhUrl).toMatch( + /^https:\/\/docs\.comfy\.org\/zh-CN\/installation\/desktop\/(windows|macos)$/ + ) + + // Test other locales default to English + const frUrl = getDesktopGuideUrl('fr') + expect(frUrl).toMatch( + /^https:\/\/docs\.comfy\.org\/installation\/desktop\/(windows|macos)$/ + ) + }) + }) + + describe('Security', () => { + it('should only use secure HTTPS for external URLs', () => { + const allUrls = [ + ...Object.values(GITHUB_REPOS), + ...Object.values(MODEL_SOURCES.repos), + ...Object.values(DEVELOPER_TOOLS), + COMFY_URLS.website.base, + COMFY_URLS.docs.base, + COMFY_URLS.community.discord + ] + + allUrls.forEach((url) => { + expect(url.startsWith('https://')).toBe(true) + expect(url.startsWith('http://')).toBe(false) + }) + }) + + it('should have valid domain allowlist for model sources', () => { + const domainPattern = /^[a-z0-9]+([-.][a-z0-9]+)*\.[a-z]{2,}$/i + + MODEL_SOURCES.allowedDomains.forEach((domain) => { + expect(domain).toMatch(domainPattern) + }) + }) + }) +})