/**
* @module HtmlSanitizer
* @file JavaScript HTML Sanitizer v2.0.4
* @author Alexander Yumashev, Jitbit Software
* @license MIT
* @description Client-side HTML sanitizer for preventing XSS in user-generated content.
* Uses DOMParser for browser-native, fast parsing with allowlist approach.
*/
"use strict";
const HtmlSanitizer = new (function () {
const _tagWhitelist = {
'A': true, 'ABBR': true, 'B': true, 'BLOCKQUOTE': true, 'BODY': true, 'BR': true, 'CENTER': true, 'CODE': true, 'DD': true, 'DIV': true, 'DL': true, 'DT': true, 'EM': true, 'FONT': true,
'H1': true, 'H2': true, 'H3': true, 'H4': true, 'H5': true, 'H6': true, 'HR': true, 'I': true, 'IMG': true, 'LABEL': true, 'LI': true, 'OL': true, 'P': true, 'PRE': true,
'SMALL': true, 'SOURCE': true, 'SPAN': true, 'STRONG': true, 'SUB': true, 'SUP': true, 'TABLE': true, 'TBODY': true, 'TR': true, 'TD': true, 'TH': true, 'THEAD': true, 'UL': true, 'U': true, 'VIDEO': true
};
const _contentTagWhiteList = { 'FORM': true, 'GOOGLE-SHEETS-HTML-ORIGIN': true };
const _attributeWhitelist = { 'align': true, 'class': true, 'color': true, 'controls': true, 'height': true, 'href': true, 'id': true, 'src': true, 'style': true, 'target': true, 'title': true, 'type': true, 'width': true };
const _cssWhitelist = { 'background-color': true, 'color': true, 'font-size': true, 'font-weight': true, 'text-align': true, 'text-decoration': true, 'width': true };
const _schemaWhiteList = [ 'http:', 'https:', 'data:', 'm-files:', 'file:', 'ftp:', 'mailto:', 'pw:' ];
const _uriAttributes = { 'href': true, 'action': true };
let _parser;
/**
* Sanitizes HTML using allowlist approach.
* @param {string} input - Raw HTML input
* @param {string} [extraSelector] - Extra CSS selector to allow
* @param {Function} [callback] - Custom filter function
* @returns {string} Sanitized HTML
*/
this.SanitizeHtml = function (input, extraSelector, callback) {
input = input.trim();
if (input == "") return "";
if (input == "<br>") return "";
if (!/<body/i.test(input)) input = "<body>" + input + "</body>";
_parser ||= new DOMParser();
let doc = _parser.parseFromString(input, "text/html");
if (doc.body.tagName !== 'BODY')
doc.body.remove();
if (typeof doc.createElement !== 'function')
doc.createElement.remove();
function makeSanitizedCopy(node) {
let newNode;
if (node.nodeType == Node.TEXT_NODE) {
newNode = node.cloneNode(true);
} else if (node.nodeType == Node.ELEMENT_NODE && (_tagWhitelist[node.tagName] || _contentTagWhiteList[node.tagName] || (extraSelector && node.matches(extraSelector))) && (!callback || callback(node))) {
if (_contentTagWhiteList[node.tagName])
newNode = doc.createElement('DIV');
else
newNode = doc.createElement(node.tagName);
for (let i = 0; i < node.attributes.length; i++) {
let attr = node.attributes[i];
if (_attributeWhitelist[attr.name]) {
if (attr.name == "style") {
for (let s = 0; s < node.style.length; s++) {
let styleName = node.style[s];
if (_cssWhitelist[styleName])
newNode.style.setProperty(styleName, node.style.getPropertyValue(styleName));
}
}
else {
if (_uriAttributes[attr.name]) {
if (attr.value.indexOf(":") > -1 && !startsWithAny(attr.value, _schemaWhiteList))
continue;
}
newNode.setAttribute(attr.name, attr.value);
}
}
}
for (let i = 0; i < node.childNodes.length; i++) {
let subCopy = makeSanitizedCopy(node.childNodes[i]);
newNode.appendChild(subCopy);
}
if ((newNode.tagName == "SPAN" || newNode.tagName == "B" || newNode.tagName == "I" || newNode.tagName == "U")
&& newNode.innerHTML.trim() == "") {
return doc.createDocumentFragment();
}
} else {
newNode = doc.createDocumentFragment();
}
return newNode;
};
let resultElement = makeSanitizedCopy(doc.body);
return resultElement.innerHTML.replace(/div><div/g, "div>\n<div");
};
function startsWithAny(str, substrings) {
for (let i = 0; i < substrings.length; i++) {
if (str.indexOf(substrings[i]) == 0) {
return true;
}
}
return false;
}
this.AllowedTags = _tagWhitelist;
this.AllowedAttributes = _attributeWhitelist;
this.AllowedCssStyles = _cssWhitelist;
this.AllowedSchemas = _schemaWhiteList;
});