-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsafeHtmlUtils.js
142 lines (128 loc) · 4.29 KB
/
safeHtmlUtils.js
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
/**
* Utility class for safely handling HTML content and preventing XSS attacks
*/
class SafeHtmlUtils {
constructor() {
// Map of characters to their HTML entity equivalents
this.htmlEntities = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
'/': '/',
'`': '`',
'=': '='
};
}
/**
* Escapes HTML special characters in a string
* @param {string} str - The string to escape
* @returns {string} The escaped string
*/
escape(str) {
try {
// Handle null, undefined, or non-string inputs
if (!str) return '';
if (typeof str !== 'string') {
str = String(str);
}
// Use regex replacement with character map
return str.replace(/[&<>'`=\/]/g, char => this.htmlEntities[char]);
} catch (error) {
console.error('Error escaping HTML:', error);
return ''; // Return empty string on error
}
}
/**
* Recursively escapes HTML in an object's string properties
* @param {Object} obj - Object containing strings to escape
* @returns {Object} New object with escaped strings
*/
escapeObject(obj) {
try {
if (!obj || typeof obj !== 'object') return {};
const escaped = {};
for (const [key, value] of Object.entries(obj)) {
if (typeof value === 'string') {
escaped[key] = this.escape(value);
} else if (Array.isArray(value)) {
escaped[key] = value.map(item =>
typeof item === 'string' ? this.escape(item) : item
);
} else if (typeof value === 'object' && value !== null) {
escaped[key] = this.escapeObject(value);
} else {
escaped[key] = value;
}
}
return escaped;
} catch (error) {
console.error('Error escaping object:', error);
return obj; // Return original object on error
}
}
/**
* Safely create an HTML element with escaped content
* @param {string} tag - The HTML tag name
* @param {Object} attributes - Object containing element attributes
* @param {string|Array} content - Content to put inside the element
* @returns {string} Safe HTML string
*/
createElement(tag, attributes = {}, content = '') {
try {
const safeTag = this.escape(tag);
const safeAttributes = Object.entries(attributes)
.map(([key, value]) => `${this.escape(key)}="${this.escape(value)}"`)
.join(' ');
const safeContent = Array.isArray(content)
? content.map(item => this.escape(item)).join('')
: this.escape(content);
return `<${safeTag} ${safeAttributes}>${safeContent}</${safeTag}>`;
} catch (error) {
console.error('Error creating element:', error);
return '';
}
}
/**
* Creates a safe anchor tag with escaped URL and content
* @param {string} url - The URL for the anchor
* @param {string} text - The link text
* @param {Object} attributes - Additional attributes for the anchor tag
* @returns {string} Safe HTML anchor tag
*/
createLink(url, text, attributes = {}) {
const safeUrl = this.escape(url);
const safeAttributes = {
...attributes,
href: safeUrl,
rel: 'noopener noreferrer' // Security best practice for external links
};
return this.createElement('a', safeAttributes, text);
}
/**
* Validates and sanitizes a CSS class string
* @param {string} classes - Space-separated CSS classes
* @returns {string} Sanitized CSS classes
*/
sanitizeClasses(classes) {
try {
if (!classes) return '';
// Remove any non-valid CSS class characters
return classes
.split(' ')
.map(cls => cls.replace(/[^a-zA-Z0-9-_]/g, ''))
.filter(Boolean)
.join(' ');
} catch (error) {
console.error('Error sanitizing classes:', error);
return '';
}
}
}
// Create a singleton instance
const safeHtml = new SafeHtmlUtils();
// Export the singleton
export default safeHtml;
// Also export the class if needed
export { SafeHtmlUtils };