-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathsanitizer.ts
More file actions
297 lines (258 loc) · 7.8 KB
/
sanitizer.ts
File metadata and controls
297 lines (258 loc) · 7.8 KB
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
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
/* eslint-disable @typescript-eslint/no-explicit-any */
/**
* This class provides methods for preparing data to sending to Hawk
* - trim long strings
* - represent html elements like <div ...> as "<div>" instead of "{}"
* - represent big objects as "<big object>"
* - represent class as <class SomeClass> or <instance of SomeClass>
*/
export default class Sanitizer {
/**
* Maximum string length
*/
private static readonly maxStringLen: number = 200;
/**
* If object in stringified JSON has more keys than this value,
* it will be represented as "<big object>"
*/
private static readonly maxObjectKeysCount: number = 20;
/**
* Maximum depth of context object
*/
private static readonly maxDepth: number = 5;
/**
* Maximum length of context arrays
*/
private static readonly maxArrayLength: number = 10;
/**
* Apply sanitizing for array/object/primitives
*
* @param data - any object to sanitize
* @param depth - current depth of recursion
* @param seen - Set of already seen objects to prevent circular references
*/
public static sanitize(data: any, depth = 0, seen = new WeakSet<object>()): any {
/**
* Check for circular references on objects and arrays
*/
if (data !== null && typeof data === 'object') {
if (seen.has(data)) {
return '<circular>';
}
seen.add(data);
}
/**
* If value is an Array, apply sanitizing for each element
*/
if (Sanitizer.isArray(data)) {
return this.sanitizeArray(data, depth + 1, seen);
/**
* If value is an Element, format it as string with outer HTML
* HTMLDivElement -> "<div ...></div>"
*/
} else if (Sanitizer.isElement(data)) {
return Sanitizer.formatElement(data);
/**
* If values is a not-constructed class, it will be formatted as "<class SomeClass>"
* class Editor {...} -> <class Editor>
*/
} else if (Sanitizer.isClassPrototype(data)) {
return Sanitizer.formatClassPrototype(data);
/**
* If values is a some class instance, it will be formatted as "<instance of SomeClass>"
* new Editor() -> <instance of Editor>
*/
} else if (Sanitizer.isClassInstance(data)) {
return Sanitizer.formatClassInstance(data);
/**
* If values is an object, do recursive call
*/
} else if (Sanitizer.isObject(data)) {
return Sanitizer.sanitizeObject(data, depth + 1, seen);
/**
* If values is a string, trim it for max-length
*/
} else if (Sanitizer.isString(data)) {
return Sanitizer.trimString(data);
}
/**
* If values is a number, boolean and other primitive, leave as is
*/
return data;
}
/**
* Apply sanitizing for each element of the array
*
* @param arr - array to sanitize
* @param depth - current depth of recursion
* @param seen - Set of already seen objects to prevent circular references
*/
private static sanitizeArray(arr: any[], depth: number, seen: WeakSet<object>): any[] {
/**
* If the maximum length is reached, slice array to max length and add a placeholder
*/
const length = arr.length;
if (length > Sanitizer.maxArrayLength) {
arr = arr.slice(0, Sanitizer.maxArrayLength);
arr.push(`<${length - Sanitizer.maxArrayLength} more items...>`);
}
return arr.map((item: any) => {
return Sanitizer.sanitize(item, depth, seen);
});
}
/**
* Process object values recursive
*
* @param data - object to beautify
* @param depth - current depth of recursion
* @param seen - Set of already seen objects to prevent circular references
*/
private static sanitizeObject(data: { [key: string]: any }, depth: number, seen: WeakSet<object>): Record<string, any> | '<deep object>' | '<big object>' {
/**
* If the maximum depth is reached, return a placeholder
*/
if (depth > Sanitizer.maxDepth) {
return '<deep object>';
}
/**
* If the object has more keys than the limit, return a placeholder
*/
if (Object.keys(data).length > Sanitizer.maxObjectKeysCount) {
return '<big object>';
}
const result: any = {};
for (const key in data) {
if (Object.prototype.hasOwnProperty.call(data, key)) {
result[key] = Sanitizer.sanitize(data[key], depth, seen);
}
}
return result;
}
/**
* Check if passed variable is an object
*
* @param target - variable to check
*/
private static isObject(target: any): boolean {
return Sanitizer.typeOf(target) === 'object';
}
/**
* Check if passed variable is an array
*
* @param target - variable to check
*/
private static isArray(target: any): boolean {
return Array.isArray(target);
}
/**
* Check if passed variable is a not-constructed class
*
* @param target - variable to check
*/
private static isClassPrototype(target: any): boolean {
if (!target || !target.constructor) {
return false;
}
/**
* like
* "function Function {
* [native code]
* }"
*/
const constructorStr = target.constructor.toString();
return constructorStr.includes('[native code]') && constructorStr.includes('Function');
}
/**
* Check if passed variable is a constructed class instance
*
* @param target - variable to check
*/
private static isClassInstance(target: any): boolean {
return target && target.constructor && (/^class \S+ {/).test(target.constructor.toString());
}
/**
* Check if passed variable is a string
*
* @param target - variable to check
*/
private static isString(target: any): boolean {
return typeof target === 'string';
}
/**
* Return string representation of the object type
*
* @param object - object to get type
*/
private static typeOf(object: any): string {
return Object.prototype.toString.call(object).match(/\s([a-zA-Z]+)/)[1].toLowerCase();
}
/**
* Check if passed variable is an HTML Element
*
* @param target - variable to check
*/
private static isElement(target: any): boolean {
return target instanceof Element;
}
/**
* Return name of a passed class
*
* @param target - not-constructed class
*/
private static getClassNameByPrototype(target: any): string {
return target.name;
}
/**
* Return name of a class by an instance
*
* @param target - instance of some class
*/
private static getClassNameByInstance(target: any): string {
return Sanitizer.getClassNameByPrototype(target.constructor);
}
/**
* Trim string if it reaches max length
*
* @param target - string to check
*/
private static trimString(target: string): string {
if (target.length > Sanitizer.maxStringLen) {
return target.substr(0, Sanitizer.maxStringLen) + '…';
}
return target;
}
/**
* Represent HTML Element as string with it outer-html
* HTMLDivElement -> "<div ...></div>"
*
* @param target - variable to format
*/
private static formatElement(target: Element): string {
/**
* Also, remove inner HTML because it can be BIG
*/
const innerHTML = target.innerHTML;
if (innerHTML) {
return target.outerHTML.replace(target.innerHTML, '…');
}
return target.outerHTML;
}
/**
* Represent not-constructed class as "<class SomeClass>"
*
* @param target - class to format
*/
private static formatClassPrototype(target: any): string {
const className = Sanitizer.getClassNameByPrototype(target);
return `<class ${className}>`;
}
/**
* Represent a some class instance as a "<instance of SomeClass>"
*
* @param target - class instance to format
*/
private static formatClassInstance(target: any): string {
const className = Sanitizer.getClassNameByInstance(target);
return `<instance of ${className}>`;
}
}