Files
ComfyUI_frontend/src/platform/workflow/templates/composables/useTemplateUrlLoader.ts
dante01yoon 0f7b3b38b0 fix(templates): address code review findings
- Derive mediaType/mediaSubtype from thumbnail_type (video support)
- Accept localized title in adaptHubWorkflowsToCategories instead of
  hardcoded 'All'
- URL loader: resolve template by name or shareId before loading
  instead of retrying on every failure
- Guard listAllHubWorkflows against cursor pagination loops
2026-03-28 23:01:03 +09:00

165 lines
5.0 KiB
TypeScript

import { useToast } from 'primevue/usetoast'
import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from 'vue-router'
import { isCloud } from '@/platform/distribution/types'
import { clearPreservedQuery } from '@/platform/navigation/preservedQueryManager'
import { PRESERVED_QUERY_NAMESPACES } from '@/platform/navigation/preservedQueryNamespaces'
import { useTelemetry } from '@/platform/telemetry'
import { useWorkflowTemplatesStore } from '@/platform/workflow/templates/repositories/workflowTemplatesStore'
// eslint-disable-next-line import-x/no-restricted-paths
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useTemplateWorkflows } from './useTemplateWorkflows'
/**
* Composable for loading templates from URL query parameters
*
* Supports URLs like:
* - /?template=flux_simple (loads with default source)
* - /?template=flux_simple&source=custom (loads from custom source)
* - /?template=flux_simple&mode=linear (loads template in linear mode)
*
* Input validation:
* - Template, source, and mode parameters must match: ^[a-zA-Z0-9_-]+$
* - Invalid formats are rejected with console warnings
*/
export function useTemplateUrlLoader() {
const route = useRoute()
const router = useRouter()
const { t } = useI18n()
const toast = useToast()
const templateWorkflows = useTemplateWorkflows()
const canvasStore = useCanvasStore()
const TEMPLATE_NAMESPACE = PRESERVED_QUERY_NAMESPACES.TEMPLATE
const SUPPORTED_MODES = ['linear'] as const
type SupportedMode = (typeof SUPPORTED_MODES)[number]
/**
* Validates parameter format to prevent path traversal and injection attacks
* Allows: letters, numbers, underscores, hyphens, and dots (for version numbers)
* Blocks: path separators (/, \), special chars that could enable injection
*/
const isValidParameter = (param: string): boolean => {
return /^[a-zA-Z0-9_.-]+$/.test(param)
}
/**
* Type guard to check if a value is a supported mode
*/
const isSupportedMode = (mode: string): mode is SupportedMode => {
return SUPPORTED_MODES.includes(mode as SupportedMode)
}
/**
* Removes template, source, and mode parameters from URL
*/
const cleanupUrlParams = () => {
const newQuery = { ...route.query }
delete newQuery.template
delete newQuery.source
delete newQuery.mode
void router.replace({ query: newQuery })
}
/**
* Loads template from URL query parameters if present
* Handles errors internally and shows appropriate user feedback
*/
const loadTemplateFromUrl = async () => {
const templateParam = route.query.template
if (!templateParam || typeof templateParam !== 'string') {
return
}
if (!isValidParameter(templateParam)) {
console.warn(
`[useTemplateUrlLoader] Invalid template parameter format: ${templateParam}`
)
return
}
const sourceParam = (route.query.source as string | undefined) || 'default'
if (!isValidParameter(sourceParam)) {
console.warn(
`[useTemplateUrlLoader] Invalid source parameter format: ${sourceParam}`
)
return
}
const modeParam = route.query.mode as string | undefined
if (
modeParam &&
(typeof modeParam !== 'string' || !isValidParameter(modeParam))
) {
console.warn(
`[useTemplateUrlLoader] Invalid mode parameter format: ${modeParam}`
)
return
}
if (modeParam && !isSupportedMode(modeParam)) {
console.warn(
`[useTemplateUrlLoader] Unsupported mode parameter: ${modeParam}. Supported modes: ${SUPPORTED_MODES.join(', ')}`
)
}
try {
await templateWorkflows.loadTemplates()
// On cloud, resolve by name or shareId before attempting to load
let resolvedName = templateParam
let resolvedSource = sourceParam
if (isCloud) {
const store = useWorkflowTemplatesStore()
const resolved =
store.getTemplateByName(templateParam) ??
store.getTemplateByShareId(templateParam)
if (resolved) {
resolvedName = resolved.name
resolvedSource = resolved.sourceModule
}
}
const success = await templateWorkflows.loadWorkflowTemplate(
resolvedName,
resolvedSource
)
if (!success) {
toast.add({
severity: 'error',
summary: t('g.error'),
detail: t('templateWorkflows.error.templateNotFound', {
templateName: templateParam
})
})
} else if (modeParam === 'linear') {
// Set linear mode after successful template load
useTelemetry()?.trackEnterLinear({ source: 'template_url' })
canvasStore.linearMode = true
}
} catch (error) {
console.error(
'[useTemplateUrlLoader] Failed to load template from URL:',
error
)
toast.add({
severity: 'error',
summary: t('g.error'),
detail: t('g.errorLoadingTemplate')
})
} finally {
cleanupUrlParams()
clearPreservedQuery(TEMPLATE_NAMESPACE)
}
}
return {
loadTemplateFromUrl
}
}