b08d76a986
Security: - S1 (backup.ts): require explicit confirmation before enabling custom-command templates from a backup file; import templates in a disabled state if declined - S2 (cookies.ts): deny non-http/https popups in cookie login window, matching the main window's setWindowOpenHandler defence - S3 (download.ts): main-process concurrency cap — startDownload now rejects spawns when active.size >= maxConcurrent (defence-in-depth; renderer already enforces this but should not be the only gate) - S4 (settings.ts, validation.ts): reject filenameTemplate values containing path traversal (.. segments or absolute paths); validate outputDir is absolute - S5 (history.ts, errorlog.ts, templates.ts, validation.ts): per-field validation on JSON reads — invalid entries dropped rather than blindly trusted Performance: - P1 (settings.ts): getSettings() now only writes to disk when sanitization actually changed a value; removes synchronous file I/O on every hot-path read - P2 (ipc.ts, download.ts, downloads.ts): renderer forwards its pre-probed metadata (title/channel/duration) via StartDownloadOptions.meta; main uses it directly instead of spawning a redundant second yt-dlp probe Maintainability: - M1 (log.ts, download.ts, probe.ts): extract duplicated cleanError() to src/main/log.ts; both callers import from the shared module - M2 (buildArgs.ts): document parseExtraArgs escape-sequence limitations - M3 (electron-builder.yml): clarify icon TODO as a pre-release action - P3 (downloads.ts): document that lowering maxConcurrent mid-flight does not pause active downloads (intended behaviour, now written down) Tests: - Add test/validation.test.ts covering isSafeFilenameTemplate, isSafeOutputDir, isValidHistoryEntry, isValidErrorLogEntry, isTemplateLike (76 tests pass) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
66 lines
2.2 KiB
TypeScript
66 lines
2.2 KiB
TypeScript
import { app } from 'electron'
|
|
import { join } from 'path'
|
|
import { readFileSync, writeFileSync, existsSync } from 'fs'
|
|
import type { CommandTemplate } from '@shared/ipc'
|
|
import { isTemplateLike } from './validation'
|
|
|
|
// Plain JSON in userData, same shape as history.ts.
|
|
const MAX_TEMPLATES = 100
|
|
|
|
function templatesFile(): string {
|
|
return join(app.getPath('userData'), 'templates.json')
|
|
}
|
|
|
|
// A persisted template entry must at least be an object carrying an id (see
|
|
// isTemplateLike in validation.ts); everything else is coerced by sanitize().
|
|
// Drop anything that isn't, so a hand-edited templates.json can't inject
|
|
// malformed entries. (audit S5)
|
|
export function listTemplates(): CommandTemplate[] {
|
|
try {
|
|
if (!existsSync(templatesFile())) return []
|
|
const data = JSON.parse(readFileSync(templatesFile(), 'utf8'))
|
|
// Validate shape, then normalise each surviving entry through sanitize() so
|
|
// name/args are always well-formed strings regardless of what was on disk.
|
|
return Array.isArray(data) ? data.filter(isTemplateLike).map(sanitize) : []
|
|
} catch {
|
|
return []
|
|
}
|
|
}
|
|
|
|
function save(templates: CommandTemplate[]): void {
|
|
try {
|
|
writeFileSync(templatesFile(), JSON.stringify(templates.slice(0, MAX_TEMPLATES), null, 2))
|
|
} catch {
|
|
/* best-effort; a read-only data dir just means no persisted templates */
|
|
}
|
|
}
|
|
|
|
function sanitize(t: CommandTemplate): CommandTemplate {
|
|
return {
|
|
id: String(t.id),
|
|
name: (t.name ?? '').trim() || 'Untitled command',
|
|
args: (t.args ?? '').trim()
|
|
}
|
|
}
|
|
|
|
/** Add a new template, or update an existing one (matched by id). */
|
|
export function saveTemplate(template: CommandTemplate): CommandTemplate[] {
|
|
const clean = sanitize(template)
|
|
const templates = [clean, ...listTemplates().filter((t) => t.id !== clean.id)]
|
|
save(templates)
|
|
return templates
|
|
}
|
|
|
|
export function removeTemplate(id: string): CommandTemplate[] {
|
|
const templates = listTemplates().filter((t) => t.id !== id)
|
|
save(templates)
|
|
return templates
|
|
}
|
|
|
|
/** Replace the entire template list (backup restore) rather than merge-by-id. */
|
|
export function replaceTemplates(templates: CommandTemplate[]): CommandTemplate[] {
|
|
const clean = templates.map(sanitize)
|
|
save(clean)
|
|
return clean
|
|
}
|