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}.
3353 *
34- * @param target - variable to check
54+ * Checked in { @link sanitize} before built-in type checks.
3555 */
36- public static isObject ( target : any ) : boolean {
37- return Sanitizer . typeOf ( target ) === 'object' ;
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).
62+ *
63+ * @param handler - handler to register
64+ */
65+ public static registerHandler ( handler : SanitizerTypeHandler ) : void {
66+ Sanitizer . customHandlers . unshift ( handler ) ;
3867 }
3968
4069 /**
@@ -45,59 +74,39 @@ 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 ) ) {
62- return this . sanitizeArray ( data , depth + 1 , seen ) ;
63-
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 ) ) {
76- return Sanitizer . formatClassPrototype ( data ) ;
77-
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 ) ) {
83- return Sanitizer . formatClassInstance ( data ) ;
84-
85- /**
86- * If values is an object, do recursive call
87- */
88- } else if ( Sanitizer . isObject ( data ) ) {
89- return Sanitizer . sanitizeObject ( data , depth + 1 , seen ) ;
90-
91- /**
92- * If values is a string, trim it for max-length
93- */
94- } else if ( Sanitizer . isString ( data ) ) {
95- return Sanitizer . trimString ( data ) ;
85+
86+ // If value is an Array, apply sanitizing for each element
87+ if ( isArray ( data ) ) return this . sanitizeArray ( data , depth + 1 , seen ) ;
88+
89+ // Check additional handlers provided by env-specific modules or users
90+ // to sanitize some additional cases (e.g. specific object types)
91+ for ( const handler of Sanitizer . customHandlers ) {
92+ if ( handler . check ( data ) ) return handler . format ( data ) ;
9693 }
9794
98- /**
99- * If values is a number, boolean and other primitive, leave as is
100- */
95+ // If values is a not-constructed class, it will be formatted as "<class SomeClass>"
96+ // class Editor {...} -> <class Editor>
97+ if ( isClassPrototype ( data ) ) return Sanitizer . formatClassPrototype ( data ) ;
98+
99+ // If values is a some class instance, it will be formatted as "<instance of SomeClass>"
100+ // new Editor() -> <instance of Editor>
101+ if ( isClassInstance ( data ) ) return Sanitizer . formatClassInstance ( data ) ;
102+
103+ // If values is an object, do recursive call
104+ if ( isPlainObject ( data ) ) return Sanitizer . sanitizeObject ( data , depth + 1 , seen ) ;
105+
106+ // If values is a string, trim it for max-length
107+ if ( isString ( data ) ) return Sanitizer . trimString ( data ) ;
108+
109+ // If values is a number, boolean and other primitive, leave as is
101110 return data ;
102111 }
103112
@@ -131,7 +140,9 @@ export default class Sanitizer {
131140 * @param depth - current depth of recursion
132141 * @param seen - Set of already seen objects to prevent circular references
133142 */
134- private static sanitizeObject ( data : { [ key : string ] : any } , depth : number , seen : WeakSet < object > ) : Record < string , any > | '<deep object>' | '<big object>' {
143+ private static sanitizeObject ( data : {
144+ [ key : string ] : any
145+ } , depth : number , seen : WeakSet < object > ) : Record < string , any > | '<deep object>' | '<big object>' {
135146 /**
136147 * If the maximum depth is reached, return a placeholder
137148 */
@@ -157,72 +168,6 @@ export default class Sanitizer {
157168 return result ;
158169 }
159170
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-
226171 /**
227172 * Return name of a passed class
228173 *
@@ -248,31 +193,12 @@ export default class Sanitizer {
248193 */
249194 private static trimString ( target : string ) : string {
250195 if ( target . length > Sanitizer . maxStringLen ) {
251- return target . substr ( 0 , Sanitizer . maxStringLen ) + '…' ;
196+ return target . substring ( 0 , Sanitizer . maxStringLen ) + '…' ;
252197 }
253198
254199 return target ;
255200 }
256201
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-
276202 /**
277203 * Represent not-constructed class as "<class SomeClass>"
278204 *
0 commit comments