Files
delphi-database/static/js/sanitizer.js
2025-08-13 18:53:35 -05:00

102 lines
3.0 KiB
JavaScript

(function () {
const DOMPURIFY_CDN = 'https://cdn.jsdelivr.net/npm/dompurify@3.0.4/dist/purify.min.js';
let _domPurifyPromise = null;
function ensureDOMPurifyLoaded() {
if (typeof window === 'undefined') {
return Promise.resolve(null);
}
if (window.DOMPurify && typeof window.DOMPurify.sanitize === 'function') {
return Promise.resolve(window.DOMPurify);
}
if (_domPurifyPromise) return _domPurifyPromise;
_domPurifyPromise = new Promise((resolve, reject) => {
try {
const script = document.createElement('script');
script.src = DOMPURIFY_CDN;
script.async = true;
script.onload = () => {
if (window.DOMPurify && window.DOMPurify.sanitize) {
resolve(window.DOMPurify);
} else {
reject(new Error('DOMPurify failed to load'));
}
};
script.onerror = () => reject(new Error('Failed to load DOMPurify'));
document.head.appendChild(script);
} catch (err) {
reject(err);
}
});
return _domPurifyPromise;
}
// Basic fallback sanitizer when DOMPurify is not available yet.
function fallbackSanitize(dirty) {
const temp = document.createElement('div');
temp.innerHTML = dirty;
// Remove script and style tags
temp.querySelectorAll('script, style').forEach((el) => el.remove());
// Remove dangerous attributes
temp.querySelectorAll('*').forEach((el) => {
Array.from(el.attributes).forEach((attr) => {
const name = attr.name;
const value = attr.value;
if (/^on/i.test(name)) {
el.removeAttribute(name);
return;
}
if ((name === 'href' || name === 'src') && value && value.trim().toLowerCase().startsWith('javascript:')) {
el.removeAttribute(name);
}
});
});
return temp.innerHTML;
}
function sanitizeHTML(dirty) {
if (typeof window !== 'undefined' && window.DOMPurify && window.DOMPurify.sanitize) {
return window.DOMPurify.sanitize(dirty);
}
// Trigger async load so the next call benefits
try {
const loader = (window && window.htmlSanitizer && typeof window.htmlSanitizer.ensureDOMPurifyLoaded === 'function')
? window.htmlSanitizer.ensureDOMPurifyLoaded
: ensureDOMPurifyLoaded;
loader().catch(() => {});
} catch (_) {}
return fallbackSanitize(dirty);
}
function escapeHtml(text) {
// Encode &, <, >, ", and '
const str = String(text == null ? '' : text);
return str
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function setSafeHTML(element, html) {
if (!element) return;
const sanitized = sanitizeHTML(String(html == null ? '' : html));
element.innerHTML = sanitized;
}
// Expose globally
window.htmlSanitizer = {
sanitize: sanitizeHTML,
ensureDOMPurifyLoaded,
escape: escapeHtml,
setHTML: setSafeHTML
};
window.setSafeHTML = setSafeHTML;
})();