mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-07 07:59:42 +02:00
fix: file viewer opens audio/svg/binary like the attachments viewer
The File Browser preview and Attachments preview share openFilePreview(), but the workspace branch (via /file-content) misclassified several types the attachments viewer handled fine: - SVG was reported as type:image, but file-raw serves SVG as octet-stream + attachment (XSS hardening), so the <img> broke. Now fetched and rendered via a same-origin image/svg+xml blob <img> (safe; <img> never runs SVG scripts). file-raw's SVG hardening is unchanged. - Audio (mp3/wav/ogg/m4a/aac/flac/opus) was type:binary -> "Cannot preview". Now classified as audio and rendered with <audio controls>; file-raw gained the matching audio/video MIME types so playback works. - Binary formats not in the hardcoded list (xlsx/doc/zip/...) were decoded as UTF-8 and dumped as mojibake. Replaced the static list with a NUL-byte content sniff that flags arbitrary binaries; the binary fallback now offers a Download link instead of dead-ending. Adds route tests for audio, known-binary (xlsx), and NUL-sniff classification. Verified end-to-end on an isolated instance + headless browser. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2503,6 +2503,25 @@ Object.assign(CodemanApp.prototype, {
|
||||
bodyEl.innerHTML = `<iframe src="${escapeHtml(rawSrc)}" title="${escapeHtml(filePath)}"></iframe>`;
|
||||
return;
|
||||
}
|
||||
// SVG renders as an image, but file-raw deliberately serves SVG as an
|
||||
// untrusted octet-stream attachment (XSS hardening), so a direct
|
||||
// <img src=file-raw> would break. Fetch the bytes and render via a
|
||||
// same-origin blob typed image/svg+xml — <img> never executes scripts in
|
||||
// the referenced SVG, so this is safe while still rendering the graphic.
|
||||
if (ext === 'svg') {
|
||||
footerEl.textContent = 'SVG';
|
||||
try {
|
||||
const res = await fetch(`/api/sessions/${sessionId}/file-raw?path=${encodeURIComponent(filePath)}`);
|
||||
if (!res.ok) throw new Error('Failed to load image');
|
||||
const blobUrl = URL.createObjectURL(new Blob([await res.text()], { type: 'image/svg+xml' }));
|
||||
bodyEl.innerHTML = `<img src="${blobUrl}" alt="${escapeHtml(filePath)}">`;
|
||||
const img = bodyEl.querySelector('img');
|
||||
if (img) img.onload = () => URL.revokeObjectURL(blobUrl);
|
||||
} catch (err) {
|
||||
bodyEl.innerHTML = `<div class="binary-message">Error: ${escapeHtml(err.message)}</div>`;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/sessions/${sessionId}/file-content?path=${encodeURIComponent(filePath)}&lines=500`);
|
||||
@@ -2519,8 +2538,12 @@ Object.assign(CodemanApp.prototype, {
|
||||
} else if (data.type === 'video') {
|
||||
bodyEl.innerHTML = `<video src="${data.url}" controls autoplay></video>`;
|
||||
footerEl.textContent = `${this.formatFileSize(data.size)} \u2022 ${data.extension}`;
|
||||
} else if (data.type === 'audio') {
|
||||
bodyEl.innerHTML = `<audio src="${data.url}" controls autoplay></audio>`;
|
||||
footerEl.textContent = `${this.formatFileSize(data.size)} \u2022 ${data.extension}`;
|
||||
} else if (data.type === 'binary') {
|
||||
bodyEl.innerHTML = `<div class="binary-message">Binary file (${this.formatFileSize(data.size)})<br>Cannot preview</div>`;
|
||||
const downloadHref = `/api/sessions/${sessionId}/file-raw?path=${encodeURIComponent(filePath)}&download=true`;
|
||||
bodyEl.innerHTML = `<div class="binary-message">Binary file (${this.formatFileSize(data.size)})<br>Cannot preview<br><a href="${escapeHtml(downloadHref)}" download>Download</a></div>`;
|
||||
footerEl.textContent = data.extension || 'binary';
|
||||
} else {
|
||||
// Text content
|
||||
|
||||
@@ -7239,12 +7239,22 @@ kbd {
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.file-preview-body audio {
|
||||
display: block;
|
||||
width: 90%;
|
||||
margin: 2rem auto;
|
||||
}
|
||||
|
||||
.file-preview-body .binary-message {
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.file-preview-body .binary-message a {
|
||||
color: var(--accent, #4ea1ff);
|
||||
}
|
||||
|
||||
.file-preview-footer {
|
||||
padding: 0.35rem 0.75rem;
|
||||
font-size: 0.7rem;
|
||||
|
||||
Reference in New Issue
Block a user