/** * GoingsOn - Escaping primitives (single source of truth) * * The CHRONIC-XSS seal lives here. Both the main app (via utils.js, which * re-exports these onto GoingsOn.utils) and the standalone compose window (which * loads this file directly) use these exact functions, so there is ONE * implementation of each escaper and the enforcement gate in js/tests/run.js * covers every caller. Load this before utils.js (main app) and before * compose-form.js (compose window). */ (function() { 'use strict'; /** * Escape HTML special characters for a text/innerHTML context. * Note: textContent serialization does NOT encode `"`, so this is NOT safe for * a double-quoted attribute value, use escapeAttrValue there. * @param {string} text * @returns {string} */ function escapeHtml(text) { if (typeof document !== 'undefined') { const div = document.createElement('div'); div.textContent = text == null ? '' : String(text); return div.innerHTML; } // Non-DOM fallback (test harness): entity-encode the HTML metacharacters. return String(text == null ? '' : text).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', })[c]); } /** * Escape a string for a raw JavaScript string-literal context (NOT an HTML * attribute). Kept private: only sound as a building block of escapeHandlerArg. * @param {string} str * @returns {string} */ function escapeJsString(str) { if (str == null) return ''; return String(str) .replace(/\\/g, '\\\\') .replace(/'/g, "\\'") .replace(/"/g, '\\"') .replace(/\n/g, '\\n') .replace(/\r/g, '\\r'); } /** * Escape a string for placement inside a double-quoted HTML attribute value. * Encodes `&` and `"` so the value cannot terminate the attribute. This is the * ONLY escaper for HTML attributes. (ultra-fuzz Run #28 C1, CHRONIC-XSS.) * @param {string} str * @returns {string} */ function escapeAttrValue(str) { return String(str ?? '') .replace(/&/g, '&') .replace(/"/g, '"'); } /** * Escape a value placed inside a quoted JS string *within* a double-quoted * inline handler, e.g. `onclick="fn('${escapeHandlerArg(x)}')"`. Survives both * the JS-string and HTML-attribute layers. * @param {string} str * @returns {string} */ function escapeHandlerArg(str) { return escapeAttrValue(escapeJsString(str)); } /** * Return a URL only if it uses a safe scheme (http/https/mailto), else "#". * Blocks `javascript:`, `data:`, and custom-handler schemes. Still pass the * result through escapeAttrValue for the attribute itself. * @param {string} url * @returns {string} */ function safeUrl(url) { const s = String(url ?? '').trim(); return /^(https?:|mailto:)/i.test(s) ? s : '#'; } const api = { escapeHtml, escapeAttrValue, escapeHandlerArg, safeUrl }; // Attach to the same GoingsOn the rest of the app references by bare identifier. // The standalone compose window loads this file with no goingson.js, so create // the namespace if it doesn't exist yet. if (typeof GoingsOn === 'undefined') { (typeof window !== 'undefined' ? window : globalThis).GoingsOn = {}; } GoingsOn.escape = api; })();