fix: HTML escaping covers attribute context

escapeHtml now escapes & < > " ' (shared web/js/html-escape.js, loaded by
admin, gallery and metadata pages) instead of the textContent/innerHTML
round-trip that left quotes intact, so a tag, category, checkpoint or file
name containing a quote can no longer break out of a data-*, value or title
attribute.

Inline onclick handlers that interpolated server data now carry data-action
and data-* attributes and are dispatched through one delegated listener per
page (web/js/data-actions.js); the img onerror fallback uses
data-fallback-src with a capturing error listener. Every remaining template
value written into an attribute or text node (prompts, checkpoints, file
paths, log lines, directory names, categories, suggestions) is escaped.

metadata.html no longer builds an "explorer /select" shell command from the
file name (Electron-only branch); the path is copied to the clipboard.
This commit is contained in:
Vito Sansevero
2026-10-08 18:01:34 -07:00
parent f99e76a135
commit edc0ee3d00
9 changed files with 379 additions and 162 deletions
+21 -37
View File
@@ -5,6 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Image Gallery - Generation Data</title>
<script src="/prompt_manager/js/theme-bridge.js"></script>
<script src="/prompt_manager/js/html-escape.js"></script>
<script src="/prompt_manager/js/comfy-metadata.js"></script>
<link rel="stylesheet" href="lib/tailwind/comfyui-theme.css">
<link rel="stylesheet" href="lib/tailwind/styles.css">
@@ -32,27 +33,13 @@
let currentWorkflowData = null;
function openInFileExplorer(path) {
// For Windows file explorer
if (navigator.platform.indexOf('Win') > -1) {
// This will work in Electron apps or desktop environments
try {
window.require('child_process').exec(`explorer /select,"${path}"`);
} catch (e) {
// Fallback: copy path to clipboard and show alert
navigator.clipboard.writeText(path).then(() => {
alert('File path copied to clipboard: ' + path);
}).catch(() => {
alert('File path: ' + path);
});
}
} else {
// For other platforms, just copy to clipboard
navigator.clipboard.writeText(path).then(() => {
alert('File path copied to clipboard: ' + path);
}).catch(() => {
alert('File path: ' + path);
});
}
// A browser page cannot open a file manager, so the path is copied instead.
// (The former Electron-only branch built a shell command from the file name.)
navigator.clipboard.writeText(path).then(() => {
alert('File path copied to clipboard: ' + path);
}).catch(() => {
alert('File path: ' + path);
});
}
async function copyToClipboard(text, button) {
@@ -213,8 +200,8 @@ Seed: ${currentWorkflowData.seed || 'Unknown'}`;
<!-- File Path -->
<div>
<h2 class="text-sm font-medium text-pm-secondary mb-2">File Path</h2>
<div class="text-sm text-pm-accent hover:bg-pm-hover cursor-pointer bg-pm-surface p-2 rounded-pm-sm break-all" onclick="openInFileExplorer('${filename}')">
${filename}
<div class="text-sm text-pm-accent hover:bg-pm-hover cursor-pointer bg-pm-surface p-2 rounded-pm-sm break-all" data-action="open-file" data-path="${escapeHtml(filename)}">
${escapeHtml(filename)}
</div>
</div>
@@ -223,7 +210,7 @@ Seed: ${currentWorkflowData.seed || 'Unknown'}`;
<h2 class="text-sm font-medium text-pm-secondary mb-2">Resources used</h2>
<div class="flex items-center justify-between">
<div>
<div class="text-pm-accent cursor-pointer">${checkpoint}</div>
<div class="text-pm-accent cursor-pointer">${escapeHtml(checkpoint)}</div>
<div class="text-xs text-pm-muted">ComfyUI Generated</div>
</div>
<span class="px-2 py-1 text-xs bg-pm-surface text-pm-secondary rounded-pm-sm">CHECKPOINT</span>
@@ -242,7 +229,7 @@ Seed: ${currentWorkflowData.seed || 'Unknown'}`;
</button>
</div>
<div class="text-sm text-pm-secondary bg-pm-surface p-3 rounded-pm-sm max-h-32 overflow-y-auto">
${positivePrompt.substring(0, 200)}${positivePrompt.length > 200 ? '...' : ''}
${escapeHtml(positivePrompt.substring(0, 200))}${positivePrompt.length > 200 ? '...' : ''}
</div>
${positivePrompt.length > 200 ? '<button class="text-pm-accent hover:bg-pm-hover text-sm mt-1" onclick="showFullPrompt(\'positive\')">Show more</button>' : ''}
</div>
@@ -258,7 +245,7 @@ Seed: ${currentWorkflowData.seed || 'Unknown'}`;
</button>
</div>
<div class="text-sm text-pm-secondary bg-pm-surface p-3 rounded-pm-sm max-h-32 overflow-y-auto">
${negativePrompt.substring(0, 200)}${negativePrompt.length > 200 ? '...' : ''}
${escapeHtml(negativePrompt.substring(0, 200))}${negativePrompt.length > 200 ? '...' : ''}
</div>
${negativePrompt.length > 200 ? '<button class="text-pm-accent hover:bg-pm-hover text-sm mt-1" onclick="showFullPrompt(\'negative\')">Show more</button>' : ''}
</div>
@@ -267,12 +254,12 @@ Seed: ${currentWorkflowData.seed || 'Unknown'}`;
<div>
<h2 class="text-sm font-medium text-pm-secondary mb-3">Other metadata</h2>
<div class="flex flex-wrap gap-2">
<span class="px-2 py-1 text-xs bg-pm-surface text-pm-secondary rounded-pm-sm">CFG SCALE: ${cfgScale}</span>
<span class="px-2 py-1 text-xs bg-pm-surface text-pm-secondary rounded-pm-sm">STEPS: ${steps}</span>
<span class="px-2 py-1 text-xs bg-pm-surface text-pm-secondary rounded-pm-sm">SAMPLER: ${sampler}</span>
<span class="px-2 py-1 text-xs bg-pm-surface text-pm-secondary rounded-pm-sm">CFG SCALE: ${escapeHtml(cfgScale)}</span>
<span class="px-2 py-1 text-xs bg-pm-surface text-pm-secondary rounded-pm-sm">STEPS: ${escapeHtml(steps)}</span>
<span class="px-2 py-1 text-xs bg-pm-surface text-pm-secondary rounded-pm-sm">SAMPLER: ${escapeHtml(sampler)}</span>
</div>
<div class="mt-2">
<span class="px-2 py-1 text-xs bg-pm-surface text-pm-secondary rounded-pm-sm">SEED: ${seed}</span>
<span class="px-2 py-1 text-xs bg-pm-surface text-pm-secondary rounded-pm-sm">SEED: ${escapeHtml(seed)}</span>
</div>
</div>
@@ -289,6 +276,9 @@ Seed: ${currentWorkflowData.seed || 'Unknown'}`;
</div>
</div>
`;
dataPanel.querySelector('[data-action="open-file"]').addEventListener('click', (e) => {
openInFileExplorer(e.currentTarget.dataset.path);
});
// Store the full data for later use
currentWorkflowData = {
@@ -304,12 +294,6 @@ Seed: ${currentWorkflowData.seed || 'Unknown'}`;
};
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function showFullPrompt(type) {
if (currentWorkflowData) {
const prompt = type === 'positive' ? currentWorkflowData.positivePrompt : currentWorkflowData.negativePrompt;
@@ -343,7 +327,7 @@ Seed: ${currentWorkflowData.seed || 'Unknown'}`;
<head><title>ComfyUI Workflow Data</title></head>
<body style="background: #111; color: #fff; font-family: monospace; padding: 20px;">
<h2>ComfyUI Workflow JSON</h2>
<pre style="background: #222; padding: 15px; border-radius: 5px; overflow: auto;">${JSON.stringify(currentWorkflowData.workflow, null, 2)}</pre>
<pre style="background: #222; padding: 15px; border-radius: 5px; overflow: auto;">${escapeHtml(JSON.stringify(currentWorkflowData.workflow, null, 2))}</pre>
</body>
</html>
`);