dompurify
Version:
DOMPurify is a DOM-only, super-fast, uber-tolerant XSS sanitizer for HTML, MathML and SVG. It's written in JavaScript and works in all modern browsers (Safari, Opera (15+), Internet Explorer (10+), Firefox and Chrome - as well as almost anything else usin
339 lines (289 loc) • 8.36 kB
text/typescript
const {
entries,
setPrototypeOf,
isFrozen,
getPrototypeOf,
getOwnPropertyDescriptor,
} = Object;
let { freeze, seal, create } = Object; // eslint-disable-line import/no-mutable-exports
let { apply, construct } = typeof Reflect !== 'undefined' && Reflect;
if (!freeze) {
freeze = function <T>(x: T): T {
return x;
};
}
if (!seal) {
seal = function <T>(x: T): T {
return x;
};
}
if (!apply) {
apply = function <T>(
func: (thisArg: any, ...args: any[]) => T,
thisArg: any,
...args: any[]
): T {
return func.apply(thisArg, args);
};
}
if (!construct) {
construct = function <T>(Func: new (...args: any[]) => T, ...args: any[]): T {
return new Func(...args);
};
}
const arrayForEach = unapply(Array.prototype.forEach);
const arrayIndexOf = unapply(Array.prototype.indexOf);
const arrayLastIndexOf = unapply(Array.prototype.lastIndexOf);
const arrayPop = unapply(Array.prototype.pop);
const arrayPush = unapply(Array.prototype.push);
const arraySlice = unapply(Array.prototype.slice);
const arraySplice = unapply(Array.prototype.splice);
const arrayIsArray = Array.isArray;
const stringToLowerCase = unapply(String.prototype.toLowerCase);
const stringToString = unapply(String.prototype.toString);
const stringMatch = unapply(String.prototype.match);
const stringReplace = unapply(String.prototype.replace);
const stringIndexOf = unapply(String.prototype.indexOf);
const stringTrim = unapply(String.prototype.trim);
const numberToString = unapply(Number.prototype.toString);
const booleanToString = unapply(Boolean.prototype.toString);
const bigintToString =
typeof BigInt === 'undefined' ? null : unapply(BigInt.prototype.toString);
const symbolToString =
typeof Symbol === 'undefined' ? null : unapply(Symbol.prototype.toString);
const objectHasOwnProperty = unapply(Object.prototype.hasOwnProperty);
const objectToString = unapply(Object.prototype.toString);
const regExpTest = unapply(RegExp.prototype.test);
const typeErrorCreate = unconstruct(TypeError);
/**
* Creates a new function that calls the given function with a specified thisArg and arguments.
*
* @param func - The function to be wrapped and called.
* @returns A new function that calls the given function with a specified thisArg and arguments.
*/
function unapply<T>(
func: (thisArg: any, ...args: any[]) => T
): (thisArg: any, ...args: any[]) => T {
return (thisArg: any, ...args: any[]): T => {
if (thisArg instanceof RegExp) {
thisArg.lastIndex = 0;
}
return apply(func, thisArg, args);
};
}
/**
* Creates a new function that constructs an instance of the given constructor function with the provided arguments.
*
* @param func - The constructor function to be wrapped and called.
* @returns A new function that constructs an instance of the given constructor function with the provided arguments.
*/
function unconstruct<T>(
Func: new (...args: any[]) => T
): (...args: any[]) => T {
return (...args: any[]): T => construct(Func, args);
}
/**
* Add properties to a lookup table
*
* @param set - The set to which elements will be added.
* @param array - The array containing elements to be added to the set.
* @param transformCaseFunc - An optional function to transform the case of each element before adding to the set.
* @returns The modified set with added elements.
*/
function addToSet(
set: Record<string, boolean>,
array: readonly unknown[],
transformCaseFunc: ReturnType<typeof unapply<string>> = stringToLowerCase
): Record<string, boolean> {
if (setPrototypeOf) {
// Make 'in' and truthy checks like Boolean(set.constructor)
// independent of any properties defined on Object.prototype.
// Prevent prototype setters from intercepting set as a this value.
setPrototypeOf(set, null);
}
if (!arrayIsArray(array)) {
return set;
}
let l = array.length;
while (l--) {
let element = array[l];
if (typeof element === 'string') {
const lcElement = transformCaseFunc(element);
if (lcElement !== element) {
// Config presets (e.g. tags.js, attrs.js) are immutable.
if (!isFrozen(array)) {
(array as unknown[])[l] = lcElement;
}
element = lcElement;
}
}
set[element as string] = true;
}
return set;
}
/**
* Clean up an array to harden against CSPP
*
* @param array - The array to be cleaned.
* @returns The cleaned version of the array
*/
function cleanArray<T>(array: T[]): Array<T | null> {
for (let index = 0; index < array.length; index++) {
const isPropertyExist = objectHasOwnProperty(array, index);
if (!isPropertyExist) {
array[index] = null;
}
}
return array;
}
/**
* Shallow clone an object
*
* @param object - The object to be cloned.
* @returns A new object that copies the original.
*/
function clone<T extends Record<string, any>>(object: T): T {
const newObject = create(null);
for (const [property, value] of entries(object)) {
const isPropertyExist = objectHasOwnProperty(object, property);
if (isPropertyExist) {
if (arrayIsArray(value)) {
newObject[property] = cleanArray(value);
} else if (
value &&
typeof value === 'object' &&
value.constructor === Object
) {
newObject[property] = clone(value);
} else {
newObject[property] = value;
}
}
}
return newObject;
}
/**
* Convert non-node values into strings without depending on direct property access.
*
* @param value - The value to stringify.
* @returns A string representation of the provided value.
*/
function stringifyValue(value: unknown): string {
switch (typeof value) {
case 'string': {
return value;
}
case 'number': {
return numberToString(value);
}
case 'boolean': {
return booleanToString(value);
}
case 'bigint': {
return bigintToString ? bigintToString(value) : '0';
}
case 'symbol': {
return symbolToString ? symbolToString(value) : 'Symbol()';
}
case 'undefined': {
return objectToString(value);
}
case 'function':
case 'object': {
if (value === null) {
return objectToString(value);
}
const valueAsRecord = value as Record<string, any>;
const valueToString = lookupGetter(valueAsRecord, 'toString');
if (typeof valueToString === 'function') {
const stringified = valueToString(valueAsRecord);
return typeof stringified === 'string'
? stringified
: objectToString(stringified);
}
return objectToString(value);
}
default: {
return objectToString(value);
}
}
}
/**
* This method automatically checks if the prop is function or getter and behaves accordingly.
*
* @param object - The object to look up the getter function in its prototype chain.
* @param prop - The property name for which to find the getter function.
* @returns The getter function found in the prototype chain or a fallback function.
*/
function lookupGetter<T extends Record<string, any>>(
object: T,
prop: string
): ReturnType<typeof unapply<any>> | (() => null) {
while (object !== null) {
const desc = getOwnPropertyDescriptor(object, prop);
if (desc) {
if (desc.get) {
return unapply(desc.get);
}
if (typeof desc.value === 'function') {
return unapply(desc.value);
}
}
object = getPrototypeOf(object);
}
function fallbackValue(): null {
return null;
}
return fallbackValue;
}
function isRegex(value: unknown): value is RegExp {
try {
regExpTest(value as RegExp, '');
return true;
} catch {
return false;
}
}
export {
// Array
arrayForEach,
arrayIndexOf,
arrayIsArray,
arrayLastIndexOf,
arrayPop,
arrayPush,
arraySlice,
arraySplice,
// Object
entries,
freeze,
getPrototypeOf,
getOwnPropertyDescriptor,
isFrozen,
setPrototypeOf,
seal,
clone,
create,
objectHasOwnProperty,
objectToString,
// RegExp
regExpTest,
isRegex,
// String
stringIndexOf,
stringMatch,
stringReplace,
stringToLowerCase,
stringToString,
stringTrim,
// Other conversion
stringifyValue,
// Errors
typeErrorCreate,
// Other
lookupGetter,
addToSet,
// Reflect
unapply,
unconstruct,
};