11/* eslint-disable @typescript-eslint/no-explicit-any */
2+ import { isArray , isClassInstance , isClassPrototype , isPlainObject , isString } from '../utils/type-guards' ;
3+
4+ /**
5+ * Custom type handler for Sanitizer.
6+ *
7+ * Allows user to register their own formatters from external packages.
8+ */
9+ export interface SanitizerTypeHandler {
10+ /**
11+ * Checks if this handler should be applied to given value
12+ *
13+ * @returns `true`
14+ */
15+ check : ( target : any ) => boolean ;
16+
17+ /**
18+ * Formats the value into a sanitized representation
19+ */
20+ format : ( target : any ) => any ;
21+ }
22+
223/**
324 * This class provides methods for preparing data to sending to Hawk
425 * - trim long strings
5- * - represent html elements like <div ...> as "<div>" instead of "{}"
626 * - represent big objects as "<big object>"
727 * - represent class as <class SomeClass> or <instance of SomeClass>
828 */
9- export default class Sanitizer {
29+ export class Sanitizer {
1030 /**
1131 * Maximum string length
1232 */
@@ -29,12 +49,21 @@ export default class Sanitizer {
2949 private static readonly maxArrayLength : number = 10 ;
3050
3151 /**
32- * Check if passed variable is an object
52+ * Custom type handlers registered via {@link registerHandler}.
53+ *
54+ * Checked in {@link sanitize} before built-in type checks.
55+ */
56+ private static readonly customHandlers : SanitizerTypeHandler [ ] = [ ] ;
57+
58+ /**
59+ * Register a custom type handler.
60+ * Handlers are checked before built-in type checks, in reverse registration order
61+ * (last registered = highest priority).
3362 *
34- * @param target - variable to check
63+ * @param handler - handler to register
3564 */
36- public static isObject ( target : any ) : boolean {
37- return Sanitizer . typeOf ( target ) === 'object' ;
65+ public static registerHandler ( handler : SanitizerTypeHandler ) : void {
66+ Sanitizer . customHandlers . unshift ( handler ) ;
3867 }
3968
4069 /**
@@ -45,59 +74,50 @@ export default class Sanitizer {
4574 * @param seen - Set of already seen objects to prevent circular references
4675 */
4776 public static sanitize ( data : any , depth = 0 , seen = new WeakSet < object > ( ) ) : any {
48- /**
49- * Check for circular references on objects and arrays
50- */
77+ // Check for circular references on objects and arrays
5178 if ( data !== null && typeof data === 'object' ) {
5279 if ( seen . has ( data ) ) {
5380 return '<circular>' ;
5481 }
5582 seen . add ( data ) ;
5683 }
5784
58- /**
59- * If value is an Array, apply sanitizing for each element
60- */
61- if ( Sanitizer . isArray ( data ) ) {
85+ // If value is an Array, apply sanitizing for each element
86+ if ( isArray ( data ) ) {
6287 return this . sanitizeArray ( data , depth + 1 , seen ) ;
88+ }
6389
64- /**
65- * If value is an Element, format it as string with outer HTML
66- * HTMLDivElement -> "<div ...></div>"
67- */
68- } else if ( Sanitizer . isElement ( data ) ) {
69- return Sanitizer . formatElement ( data ) ;
70-
71- /**
72- * If values is a not-constructed class, it will be formatted as "<class SomeClass>"
73- * class Editor {...} -> <class Editor>
74- */
75- } else if ( Sanitizer . isClassPrototype ( data ) ) {
90+ // Check additional handlers provided by env-specific modules or users
91+ // to sanitize some additional cases (e.g. specific object types)
92+ for ( const handler of Sanitizer . customHandlers ) {
93+ if ( handler . check ( data ) ) {
94+ return handler . format ( data ) ;
95+ }
96+ }
97+
98+ // If values is a not-constructed class, it will be formatted as "<class SomeClass>"
99+ // class Editor {...} -> <class Editor>
100+ if ( isClassPrototype ( data ) ) {
76101 return Sanitizer . formatClassPrototype ( data ) ;
102+ }
77103
78- /**
79- * If values is a some class instance, it will be formatted as "<instance of SomeClass>"
80- * new Editor() -> <instance of Editor>
81- */
82- } else if ( Sanitizer . isClassInstance ( data ) ) {
104+ // If values is a some class instance, it will be formatted as "<instance of SomeClass>"
105+ // new Editor() -> <instance of Editor>
106+ if ( isClassInstance ( data ) ) {
83107 return Sanitizer . formatClassInstance ( data ) ;
108+ }
84109
85- /**
86- * If values is an object, do recursive call
87- */
88- } else if ( Sanitizer . isObject ( data ) ) {
110+ // If values is an object, do recursive call
111+ if ( isPlainObject ( data ) ) {
89112 return Sanitizer . sanitizeObject ( data , depth + 1 , seen ) ;
113+ }
90114
91- /**
92- * If values is a string, trim it for max-length
93- */
94- } else if ( Sanitizer . isString ( data ) ) {
115+ // If values is a string, trim it for max-length
116+ if ( isString ( data ) ) {
95117 return Sanitizer . trimString ( data ) ;
96118 }
97119
98- /**
99- * If values is a number, boolean and other primitive, leave as is
100- */
120+ // If values is a number, boolean and other primitive, leave as is
101121 return data ;
102122 }
103123
@@ -109,9 +129,7 @@ export default class Sanitizer {
109129 * @param seen - Set of already seen objects to prevent circular references
110130 */
111131 private static sanitizeArray ( arr : any [ ] , depth : number , seen : WeakSet < object > ) : any [ ] {
112- /**
113- * If the maximum length is reached, slice array to max length and add a placeholder
114- */
132+ // If the maximum length is reached, slice array to max length and add a placeholder
115133 const length = arr . length ;
116134
117135 if ( length > Sanitizer . maxArrayLength ) {
@@ -131,17 +149,18 @@ export default class Sanitizer {
131149 * @param depth - current depth of recursion
132150 * @param seen - Set of already seen objects to prevent circular references
133151 */
134- private static sanitizeObject ( data : { [ key : string ] : any } , depth : number , seen : WeakSet < object > ) : Record < string , any > | '<deep object>' | '<big object>' {
135- /**
136- * If the maximum depth is reached, return a placeholder
137- */
152+ private static sanitizeObject (
153+ data : { [ key : string ] : any } ,
154+ depth : number ,
155+ seen : WeakSet < object >
156+ ) : Record < string , any > | '<deep object>' | '<big object>' {
157+
158+ // If the maximum depth is reached, return a placeholder
138159 if ( depth > Sanitizer . maxDepth ) {
139160 return '<deep object>' ;
140161 }
141162
142- /**
143- * If the object has more keys than the limit, return a placeholder
144- */
163+ // If the object has more keys than the limit, return a placeholder
145164 if ( Object . keys ( data ) . length > Sanitizer . maxObjectKeysCount ) {
146165 return '<big object>' ;
147166 }
@@ -157,72 +176,6 @@ export default class Sanitizer {
157176 return result ;
158177 }
159178
160- /**
161- * Check if passed variable is an array
162- *
163- * @param target - variable to check
164- */
165- private static isArray ( target : any ) : boolean {
166- return Array . isArray ( target ) ;
167- }
168-
169- /**
170- * Check if passed variable is a not-constructed class
171- *
172- * @param target - variable to check
173- */
174- private static isClassPrototype ( target : any ) : boolean {
175- if ( ! target || ! target . constructor ) {
176- return false ;
177- }
178-
179- /**
180- * like
181- * "function Function {
182- * [native code]
183- * }"
184- */
185- const constructorStr = target . constructor . toString ( ) ;
186-
187- return constructorStr . includes ( '[native code]' ) && constructorStr . includes ( 'Function' ) ;
188- }
189-
190- /**
191- * Check if passed variable is a constructed class instance
192- *
193- * @param target - variable to check
194- */
195- private static isClassInstance ( target : any ) : boolean {
196- return target && target . constructor && ( / ^ c l a s s \S + { / ) . test ( target . constructor . toString ( ) ) ;
197- }
198-
199- /**
200- * Check if passed variable is a string
201- *
202- * @param target - variable to check
203- */
204- private static isString ( target : any ) : boolean {
205- return typeof target === 'string' ;
206- }
207-
208- /**
209- * Return string representation of the object type
210- *
211- * @param object - object to get type
212- */
213- private static typeOf ( object : any ) : string {
214- return Object . prototype . toString . call ( object ) . match ( / \s ( [ a - z A - Z ] + ) / ) [ 1 ] . toLowerCase ( ) ;
215- }
216-
217- /**
218- * Check if passed variable is an HTML Element
219- *
220- * @param target - variable to check
221- */
222- private static isElement ( target : any ) : boolean {
223- return target instanceof Element ;
224- }
225-
226179 /**
227180 * Return name of a passed class
228181 *
@@ -248,31 +201,12 @@ export default class Sanitizer {
248201 */
249202 private static trimString ( target : string ) : string {
250203 if ( target . length > Sanitizer . maxStringLen ) {
251- return target . substr ( 0 , Sanitizer . maxStringLen ) + '…' ;
204+ return target . substring ( 0 , Sanitizer . maxStringLen ) + '…' ;
252205 }
253206
254207 return target ;
255208 }
256209
257- /**
258- * Represent HTML Element as string with it outer-html
259- * HTMLDivElement -> "<div ...></div>"
260- *
261- * @param target - variable to format
262- */
263- private static formatElement ( target : Element ) : string {
264- /**
265- * Also, remove inner HTML because it can be BIG
266- */
267- const innerHTML = target . innerHTML ;
268-
269- if ( innerHTML ) {
270- return target . outerHTML . replace ( target . innerHTML , '…' ) ;
271- }
272-
273- return target . outerHTML ;
274- }
275-
276210 /**
277211 * Represent not-constructed class as "<class SomeClass>"
278212 *
0 commit comments