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.
43 lines
1.4 KiB
JavaScript
43 lines
1.4 KiB
JavaScript
/**
|
|
* HTML escaping for the admin, gallery and metadata pages.
|
|
*
|
|
* Escapes the five characters that matter in both text and attribute context
|
|
* (& < > " '), so one function is safe for `innerHTML` text nodes and for
|
|
* `data-*` / `value` / `title` attributes alike. The old `textContent` ->
|
|
* `innerHTML` trick left quotes untouched and let a tag such as
|
|
* `" onmouseover="alert(1)` break out of an attribute.
|
|
*
|
|
* Loadable in the browser (window.escapeHtml / window.HtmlEscape) and in Node
|
|
* for tests.
|
|
*/
|
|
(function (root) {
|
|
"use strict";
|
|
|
|
const REPLACEMENTS = Object.freeze({
|
|
"&": "&",
|
|
"<": "<",
|
|
">": ">",
|
|
'"': """,
|
|
"'": "'",
|
|
});
|
|
const UNSAFE = /[&<>"']/g;
|
|
|
|
/**
|
|
* @param {unknown} value - anything; null/undefined become "", others go through String()
|
|
* @returns {string} text safe to interpolate into HTML text or a quoted attribute
|
|
*/
|
|
function escapeHtml(value) {
|
|
if (value === null || value === undefined) return "";
|
|
return String(value).replace(UNSAFE, (ch) => REPLACEMENTS[ch]);
|
|
}
|
|
|
|
const api = { escapeHtml, escapeAttr: escapeHtml };
|
|
if (typeof module !== "undefined" && module.exports) {
|
|
module.exports = api;
|
|
} else {
|
|
root.HtmlEscape = api;
|
|
root.escapeHtml = escapeHtml;
|
|
root.escapeAttr = escapeHtml;
|
|
}
|
|
})(typeof window !== "undefined" ? window : globalThis);
|