/**
* HTML Sanitization Utility
* Prevents XSS attacks by escaping HTML special characters
*/
/**
* Escape HTML special characters to prevent XSS
* @param {string|number} text - Text to escape
* @param {boolean} forAttribute - If true, also escapes quotes for HTML attributes
* @returns {string} Escaped HTML string
*/
export function escapeHtml(text, forAttribute = false) {
if (text === null || text === undefined) {
return '';
}
const str = String(text);
const map = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
};
let escaped = str.replace(/[&<>"']/g, m => map[m]);
// For attributes, ensure quotes are properly escaped
if (forAttribute) {
escaped = escaped.replace(/"/g, '"').replace(/'/g, ''');
}
return escaped;
}
/**
* Safely set innerHTML with sanitization
* @param {HTMLElement} element - DOM element to update
* @param {string} html - HTML string (will be sanitized)
*/
export function safeSetInnerHTML(element, html) {
if (!element || !(element instanceof HTMLElement)) {
console.warn('[Sanitizer] Invalid element provided to safeSetInnerHTML');
return;
}
// For simple text content, use textContent instead
if (!html.includes('<') && !html.includes('>')) {
element.textContent = html;
return;
}
// For HTML content, create a temporary container and sanitize
const temp = document.createElement('div');
temp.innerHTML = html;
// Sanitize all text nodes
const walker = document.createTreeWalker(
temp,
NodeFilter.SHOW_TEXT,
null,
false
);
let node;
while (node = walker.nextNode()) {
if (node.textContent) {
node.textContent = node.textContent; // Already safe, but ensure it's set
}
}
// Clear and append sanitized content
element.innerHTML = '';
while (temp.firstChild) {
element.appendChild(temp.firstChild);
}
}
/**
* Sanitize object values for HTML rendering
* Recursively escapes string values in objects
* @param {any} obj - Object to sanitize
* @param {number} depth - Recursion depth limit
* @returns {any} Sanitized object
*/
export function sanitizeObject(obj, depth = 5) {
if (depth <= 0) {
return '[Max Depth Reached]';
}
if (obj === null || obj === undefined) {
return '';
}
if (typeof obj === 'string') {
return escapeHtml(obj);
}
if (typeof obj === 'number' || typeof obj === 'boolean') {
return obj;
}
if (Array.isArray(obj)) {
return obj.map(item => sanitizeObject(item, depth - 1));
}
if (typeof obj === 'object') {
const sanitized = {};
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
sanitized[key] = sanitizeObject(obj[key], depth - 1);
}
}
return sanitized;
}
return String(obj);
}
/**
* Format number safely for display
* @param {number} value - Number to format
* @param {object} options - Formatting options
* @returns {string} Formatted number
*/
export function safeFormatNumber(value, options = {}) {
if (value === null || value === undefined || isNaN(value)) {
return '—';
}
const num = Number(value);
if (isNaN(num)) {
return '—';
}
try {
return num.toLocaleString('en-US', {
minimumFractionDigits: options.minimumFractionDigits || 2,
maximumFractionDigits: options.maximumFractionDigits || 2,
...options
});
} catch (error) {
console.warn('[Sanitizer] Number formatting error:', error);
return String(num);
}
}
/**
* Safely format currency
* @param {number} value - Currency value
* @param {string} currency - Currency code (default: USD)
* @returns {string} Formatted currency string
*/
export function safeFormatCurrency(value, currency = 'USD') {
if (value === null || value === undefined || isNaN(value)) {
return '—';
}
const num = Number(value);
if (isNaN(num)) {
return '—';
}
try {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: currency,
minimumFractionDigits: 2,
maximumFractionDigits: 2
}).format(num);
} catch (error) {
console.warn('[Sanitizer] Currency formatting error:', error);
return `$${num.toFixed(2)}`;
}
}