File size: 4,855 Bytes
b190b45
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
/**

 * 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 = {
        '&': '&',
        '<': '&lt;',
        '>': '&gt;',
        '"': '&quot;',
        "'": '&#039;'
    };
    
    let escaped = str.replace(/[&<>"']/g, m => map[m]);
    
    // For attributes, ensure quotes are properly escaped
    if (forAttribute) {
        escaped = escaped.replace(/"/g, '&quot;').replace(/'/g, '&#039;');
    }
    
    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)}`;
    }
}