fix(presets): apply preset DPI and prevent innerHTML XSS injection in preset manager (fixes #113, fixes #114) #122

Merged
gronod merged 1 commits from fix/presets-dpi-xss into development 2026-08-28 18:54:13 +01:00
4 changed files with 115 additions and 37 deletions
+34 -1
View File
@@ -150,7 +150,24 @@ pub fn export_preset_json(preset: ProfilingPreset) -> Result<String, String> {
#[tauri::command]
pub fn import_preset_json(json: String) -> Result<ProfilingPreset, String> {
serde_json::from_str::<ProfilingPreset>(&json).map_err(|e| format!("Invalid preset format: {}", e))
let preset: ProfilingPreset = serde_json::from_str::<ProfilingPreset>(&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 {
+5
View File
@@ -176,6 +176,11 @@
<label><input type="radio" name="bitDepth" value="16"> 16-bit</label>
</div>
</div>
<div class="form-group">
<label for="tiffDpi">Target Resolution (DPI)</label>
<input type="number" id="tiffDpi" min="72" max="600" value="300" placeholder="300">
</div>
</div>
<button class="primary" id="btnCreateLayout">Create Layout (.ti2 + .tif)</button>
+74 -35
View File
@@ -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 = `
<div style="flex:1; margin-right:12px;">
<div style="font-weight:600; font-size:0.9rem;">${p.name} ${isBuiltin ? '<span style="font-size:0.7rem; opacity:0.6; border:1px solid #555; padding:1px 4px; border-radius:3px; margin-left:4px;">Built-in</span>' : ''}</div>
<div style="font-size:0.75rem; opacity:0.7; margin-top:2px;">${p.description || "No description"}</div>
<div style="font-size:0.7rem; opacity:0.5; margin-top:2px;">${p.colour_space.toUpperCase()}${p.patch_count} patches • ${p.page_size}${p.bit_depth}-bit • Quality ${p.colprof_quality.toUpperCase()}</div>
</div>
<div style="display:flex; gap:6px;">
<button type="button" class="secondary btn-export-one" data-id="${p.id}" style="font-size:0.75rem; padding:4px 8px;">Export</button>
${!isBuiltin ? `<button type="button" class="danger btn-delete-one" data-id="${p.id}" style="font-size:0.75rem; padding:4px 8px;">Delete</button>` : ''}
</div>
`;
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,
+2 -1
View File
@@ -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,
};