From f0e8a7d9d9e9babaacce4113ac564c66dd30a260 Mon Sep 17 00:00:00 2001 From: gronod Date: Fri, 28 Aug 2026 18:53:50 +0100 Subject: [PATCH] fix(presets): apply preset DPI and prevent innerHTML XSS injection in preset manager (fixes #113, fixes #114) --- src-tauri/src/settings.rs | 35 +++++++++++- src/index.html | 5 ++ src/js/presets.js | 109 ++++++++++++++++++++++++++------------ src/js/printtarg.js | 3 +- 4 files changed, 115 insertions(+), 37 deletions(-) diff --git a/src-tauri/src/settings.rs b/src-tauri/src/settings.rs index c444aec..c45a2a9 100644 --- a/src-tauri/src/settings.rs +++ b/src-tauri/src/settings.rs @@ -150,7 +150,24 @@ pub fn export_preset_json(preset: ProfilingPreset) -> Result { #[tauri::command] pub fn import_preset_json(json: String) -> Result { - serde_json::from_str::(&json).map_err(|e| format!("Invalid preset format: {}", e)) + let preset: ProfilingPreset = serde_json::from_str::(&json) + .map_err(|e| format!("Invalid preset format: {}", e))?; + + if preset.name.trim().is_empty() { + return Err("Preset name cannot be empty".to_string()); + } + if preset.name.len() > 200 { + return Err("Preset name is too long (max 200 characters)".to_string()); + } + if let Some(ref desc) = preset.description { + if desc.len() > 500 { + return Err("Preset description is too long (max 500 characters)".to_string()); + } + } + if preset.dpi < 72 || preset.dpi > 2400 { + return Err("Preset DPI out of allowed range (72 - 2400)".to_string()); + } + Ok(preset) } #[cfg(test)] @@ -168,6 +185,22 @@ mod tests { assert_eq!(defaults[1].patch_count, 1500); } + #[test] + fn test_import_preset_validation_name_length() { + let mut preset = get_default_presets()[0].clone(); + preset.name = "a".repeat(201); + let json = serde_json::to_string(&preset).unwrap(); + assert!(import_preset_json(json).is_err()); + } + + #[test] + fn test_import_preset_validation_dpi() { + let mut preset = get_default_presets()[0].clone(); + preset.dpi = 10; + let json = serde_json::to_string(&preset).unwrap(); + assert!(import_preset_json(json).is_err()); + } + #[test] fn test_preset_json_export_and_import() { let preset = ProfilingPreset { diff --git a/src/index.html b/src/index.html index 76dfddf..207bd9f 100644 --- a/src/index.html +++ b/src/index.html @@ -176,6 +176,11 @@ + +
+ + +
diff --git a/src/js/presets.js b/src/js/presets.js index fcb8068..0b5e9f8 100644 --- a/src/js/presets.js +++ b/src/js/presets.js @@ -50,44 +50,75 @@ export async function initPresets() { const item = document.createElement("div"); item.style.cssText = "display:flex; justify-content:space-between; align-items:center; background:rgba(255,255,255,0.05); padding:8px 12px; border-radius:6px;"; - item.innerHTML = ` -
-
${p.name} ${isBuiltin ? 'Built-in' : ''}
-
${p.description || "No description"}
-
${p.colour_space.toUpperCase()} • ${p.patch_count} patches • ${p.page_size} • ${p.bit_depth}-bit • Quality ${p.colprof_quality.toUpperCase()}
-
-
- - ${!isBuiltin ? `` : ''} -
- `; + const leftCol = document.createElement("div"); + leftCol.style.cssText = "flex:1; margin-right:12px;"; - managePresetsList.appendChild(item); - }); + const titleRow = document.createElement("div"); + titleRow.style.cssText = "font-weight:600; font-size:0.9rem; display:flex; align-items:center; gap:6px;"; + const titleSpan = document.createElement("span"); + titleSpan.textContent = p.name || "Untitled"; + titleRow.appendChild(titleSpan); - // Attach listeners for export / delete - managePresetsList.querySelectorAll(".btn-export-one").forEach((btn) => { - btn.addEventListener("click", async () => { - const id = btn.getAttribute("data-id"); - const preset = currentPresets.find(p => p.id === id); - if (preset) exportPreset(preset); - }); - }); + if (isBuiltin) { + const badge = document.createElement("span"); + badge.style.cssText = "font-size:0.7rem; opacity:0.6; border:1px solid #555; padding:1px 4px; border-radius:3px;"; + badge.textContent = "Built-in"; + titleRow.appendChild(badge); + } + leftCol.appendChild(titleRow); - managePresetsList.querySelectorAll(".btn-delete-one").forEach((btn) => { - btn.addEventListener("click", async () => { - const id = btn.getAttribute("data-id"); - if (confirm("Delete this custom preset?")) { - try { - currentPresets = await invoke("delete_preset", { id }); - if (activePresetId === id) activePresetId = currentPresets[0]?.id || "preset-std-rgb"; - renderPresetDropdown(); - renderManagePresetsList(); - } catch (err) { - alert("Delete failed: " + err); + const descDiv = document.createElement("div"); + descDiv.style.cssText = "font-size:0.8rem; opacity:0.75; margin-top:2px;"; + descDiv.textContent = p.description || "No description"; + leftCol.appendChild(descDiv); + + const metaDiv = document.createElement("div"); + metaDiv.style.cssText = "font-size:0.75rem; opacity:0.5; margin-top:4px;"; + metaDiv.textContent = `${(p.colour_space || "").toUpperCase()} • ${p.patch_count} patches • ${p.page_size} • ${p.dpi || 300} DPI • ${p.bit_depth}-bit`; + leftCol.appendChild(metaDiv); + + item.appendChild(leftCol); + + const actionsDiv = document.createElement("div"); + actionsDiv.style.cssText = "display:flex; gap:6px;"; + + const exportBtn = document.createElement("button"); + exportBtn.type = "button"; + exportBtn.className = "icon-btn"; + exportBtn.title = "Export to JSON"; + exportBtn.style.cssText = "font-size:0.85rem; padding:4px 6px; border:1px solid var(--border-color, #333); border-radius:4px; background:transparent; cursor:pointer;"; + exportBtn.textContent = "💾"; + exportBtn.addEventListener("click", () => exportPreset(p)); + actionsDiv.appendChild(exportBtn); + + if (!isBuiltin) { + const deleteBtn = document.createElement("button"); + deleteBtn.type = "button"; + deleteBtn.className = "icon-btn"; + deleteBtn.title = "Delete Preset"; + deleteBtn.style.cssText = "font-size:0.85rem; padding:4px 6px; border:1px solid var(--border-color, #333); border-radius:4px; background:transparent; cursor:pointer; color:#ff6b6b;"; + deleteBtn.textContent = "🗑️"; + deleteBtn.addEventListener("click", async () => { + if (confirm(`Delete preset "${p.name}"?`)) { + try { + currentPresets = await invoke("delete_preset", { id: p.id }); + if (activePresetId === p.id) { + activePresetId = "preset-std-rgb"; + const fallback = currentPresets.find(x => x.id === activePresetId); + if (fallback) applyPreset(fallback); + } + renderPresetDropdown(); + renderManagePresetsList(); + } catch (err) { + alert("Delete preset failed: " + err); + } } - } - }); + }); + actionsDiv.appendChild(deleteBtn); + } + + item.appendChild(actionsDiv); + managePresetsList.appendChild(item); }); } @@ -133,6 +164,11 @@ export async function initPresets() { if (Number(r.value) === preset.bit_depth) r.checked = true; }); + const tiffDpi = document.getElementById("tiffDpi"); + if (tiffDpi && preset.dpi) { + tiffDpi.value = preset.dpi; + } + // Stage 4 controls const colprofQuality = document.getElementById("colprofQuality"); if (colprofQuality && preset.colprof_quality) colprofQuality.value = preset.colprof_quality; @@ -171,6 +207,9 @@ export async function initPresets() { const bitDepthRadio = document.querySelector('input[name="bitDepth"]:checked'); const bit_depth = bitDepthRadio ? parseInt(bitDepthRadio.value, 10) : 8; + const tiffDpi = document.getElementById("tiffDpi"); + const dpi = tiffDpi && tiffDpi.value ? parseInt(tiffDpi.value, 10) || 300 : 300; + const colprofQuality = document.getElementById("colprofQuality"); const colprof_quality = colprofQuality ? colprofQuality.value : "m"; @@ -188,7 +227,7 @@ export async function initPresets() { instrument, page_size, bit_depth, - dpi: 300, + dpi, colprof_algorithm, colprof_quality, colprof_intent: null, diff --git a/src/js/printtarg.js b/src/js/printtarg.js index 3222048..61f6520 100644 --- a/src/js/printtarg.js +++ b/src/js/printtarg.js @@ -301,11 +301,12 @@ export function initPrinttarg() { if (radio.checked) bitDepth = parseInt(radio.value, 10); }); + const dpi = tiffDpi && tiffDpi.value ? parseInt(tiffDpi.value, 10) || 300 : 300; const config = { instrument: instrumentSelect.value, page_size: pageSize, bit_depth: bitDepth, - dpi: 300, // Master high-resolution layout scaled automatically to device bounds + dpi: dpi, basename: stage1Basename, cwd: stage1Cwd, }; -- 2.39.5