92 lines
2.7 KiB
JavaScript
92 lines
2.7 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
|
|
ensureDOMPurifyLoaded().catch(() => {});
|
|
return fallbackSanitize(dirty);
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
const span = document.createElement('span');
|
|
span.textContent = String(text == null ? '' : text);
|
|
return span.innerHTML;
|
|
}
|
|
|
|
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;
|
|
})();
|