mirror of
https://github.com/Cacti/cacti.git
synced 2026-10-26 21:18:40 +09:00
Commit 8471 bumped dompurify from 3.4.16 to 3.4.17 in package.json but left the JavaScript contract test and the vendored include/js/purify.js at 3.4.16, breaking the "JavaScript contracts" CI job on develop (test "DOMPurify is pinned and generated from the matching npm release"). - Update tests/js/sync_js.test.mjs to expect dompurify 3.4.17 in both the package.json pin assertion and the purify.js license-header match. - Regenerate include/js/purify.js and include/js/purify.js.map from the dompurify 3.4.17 npm release via build/sync-js.mjs (npm install postinstall). All 78 JavaScript contract tests pass locally. Co-authored-by: TheWitness <copilot@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2544 lines
87 KiB
JavaScript
2544 lines
87 KiB
JavaScript
/*! @license DOMPurify 3.4.17 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.4.17/LICENSE */
|
|
(function(global, factory) {
|
|
typeof exports === "object" && typeof module !== "undefined" ? module.exports = factory() : typeof define === "function" && define.amd ? define([], factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self, global.DOMPurify = factory());
|
|
})(this, function() {
|
|
"use strict";
|
|
function _OverloadYield(e, d) {
|
|
this.v = e, this.k = d;
|
|
}
|
|
function _arrayLikeToArray(r, a) {
|
|
(null == a || a > r.length) && (a = r.length);
|
|
for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e];
|
|
return n;
|
|
}
|
|
function _arrayWithHoles(r) {
|
|
if (Array.isArray(r)) return r;
|
|
}
|
|
function _iterableToArrayLimit(r, l) {
|
|
var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"];
|
|
if (null != t) {
|
|
var e, n, i, u, a = [], f = !0, o = !1;
|
|
try {
|
|
if (i = (t = t.call(r)).next, 0 === l) {
|
|
if (Object(t) !== t) return;
|
|
f = !1;
|
|
} else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0);
|
|
} catch (r) {
|
|
o = !0, n = r;
|
|
} finally {
|
|
try {
|
|
if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return;
|
|
} finally {
|
|
if (o) throw n;
|
|
}
|
|
}
|
|
return a;
|
|
}
|
|
}
|
|
function _nonIterableRest() {
|
|
throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
|
|
}
|
|
/*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/babel/babel/blob/main/packages/babel-helpers/LICENSE */
|
|
function _slicedToArray(r, e) {
|
|
return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest();
|
|
}
|
|
function _unsupportedIterableToArray(r, a) {
|
|
if (r) {
|
|
if ("string" == typeof r) return _arrayLikeToArray(r, a);
|
|
var t = {}.toString.call(r).slice(8, -1);
|
|
return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0;
|
|
}
|
|
}
|
|
function AsyncGenerator(e) {
|
|
var t, n;
|
|
function resume(t, n) {
|
|
try {
|
|
var r = e[t](n), o = r.value, u = o instanceof _OverloadYield;
|
|
Promise.resolve(u ? o.v : o).then(function(n) {
|
|
if (u) {
|
|
var i = "return" === t && o.k ? t : "next";
|
|
if (!o.k || n.done) return resume(i, n);
|
|
n = e[i](n).value;
|
|
}
|
|
settle(!!r.done, n);
|
|
}, function(e) {
|
|
resume("throw", e);
|
|
});
|
|
} catch (e) {
|
|
settle(2, e);
|
|
}
|
|
}
|
|
function settle(e, r) {
|
|
2 === e ? t.reject(r) : t.resolve({
|
|
value: r,
|
|
done: e
|
|
}), (t = t.next) ? resume(t.key, t.arg) : n = null;
|
|
}
|
|
this._invoke = function(e, r) {
|
|
return new Promise(function(o, u) {
|
|
var i = {
|
|
key: e,
|
|
arg: r,
|
|
resolve: o,
|
|
reject: u,
|
|
next: null
|
|
};
|
|
n ? n = n.next = i : (t = n = i, resume(e, r));
|
|
});
|
|
}, "function" != typeof e.return && (this.return = void 0);
|
|
}
|
|
AsyncGenerator.prototype["function" == typeof Symbol && Symbol.asyncIterator || "@@asyncIterator"] = function() {
|
|
return this;
|
|
}, AsyncGenerator.prototype.next = function(e) {
|
|
return this._invoke("next", e);
|
|
}, AsyncGenerator.prototype.throw = function(e) {
|
|
return this._invoke("throw", e);
|
|
}, AsyncGenerator.prototype.return = function(e) {
|
|
return this._invoke("return", e);
|
|
};
|
|
const entries = Object.entries;
|
|
const setPrototypeOf = Object.setPrototypeOf;
|
|
const isFrozen = Object.isFrozen;
|
|
const getPrototypeOf = Object.getPrototypeOf;
|
|
const getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
|
|
let freeze = Object.freeze;
|
|
let seal = Object.seal;
|
|
let create = Object.create;
|
|
let _ref = typeof Reflect !== "undefined" && Reflect;
|
|
let apply = _ref.apply;
|
|
let construct = _ref.construct;
|
|
if (!freeze) freeze = function freeze(x) {
|
|
return x;
|
|
};
|
|
if (!seal) seal = function seal(x) {
|
|
return x;
|
|
};
|
|
if (!apply) apply = function apply(func, thisArg) {
|
|
for (var _len = arguments.length, args = new Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) args[_key - 2] = arguments[_key];
|
|
return func.apply(thisArg, args);
|
|
};
|
|
if (!construct) construct = function construct(Func) {
|
|
for (var _len2 = arguments.length, args = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) args[_key2 - 1] = arguments[_key2];
|
|
return new Func(...args);
|
|
};
|
|
const arrayForEach = unapply(Array.prototype.forEach);
|
|
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 setCreate = unconstruct(Set);
|
|
const setAdd = unapply(Set.prototype.add);
|
|
const setHas = unapply(Set.prototype.has);
|
|
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(func) {
|
|
return function(thisArg) {
|
|
if (thisArg instanceof RegExp) thisArg.lastIndex = 0;
|
|
for (var _len3 = arguments.length, args = new Array(_len3 > 1 ? _len3 - 1 : 0), _key3 = 1; _key3 < _len3; _key3++) args[_key3 - 1] = arguments[_key3];
|
|
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(Func) {
|
|
return function() {
|
|
for (var _len4 = arguments.length, args = new Array(_len4), _key4 = 0; _key4 < _len4; _key4++) args[_key4] = arguments[_key4];
|
|
return 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, array) {
|
|
let transformCaseFunc = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : stringToLowerCase;
|
|
if (setPrototypeOf) 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) {
|
|
if (!isFrozen(array)) array[l] = lcElement;
|
|
element = lcElement;
|
|
}
|
|
}
|
|
set[element] = 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(array) {
|
|
for (let index = 0; index < array.length; index++) if (!objectHasOwnProperty(array, index)) 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(object) {
|
|
const newObject = create(null);
|
|
for (const _ref2 of entries(object)) {
|
|
var _ref3 = _slicedToArray(_ref2, 2);
|
|
const property = _ref3[0];
|
|
const value = _ref3[1];
|
|
if (objectHasOwnProperty(object, property)) {
|
|
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) {
|
|
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;
|
|
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(object, prop) {
|
|
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() {
|
|
return null;
|
|
}
|
|
return fallbackValue;
|
|
}
|
|
/**
|
|
* Like lookupGetter, but for accessor properties only, and returns null
|
|
* instead of a null-returning fallback when no getter exists on the chain.
|
|
* Lets a caller fall back to a direct property read in environments that
|
|
* lack the getter, where a fallback returning null would be indistinguishable
|
|
* from a legitimate null value (e.g. namespaceURI of a no-namespace element).
|
|
*
|
|
* @param object - The object (usually a prototype) to start the lookup at.
|
|
* @param prop - The accessor property name.
|
|
* @returns The unapplied getter, or null if none was found.
|
|
*/
|
|
function lookupAccessor(object, prop) {
|
|
while (object !== null) {
|
|
const desc = getOwnPropertyDescriptor(object, prop);
|
|
if (desc) return desc.get ? unapply(desc.get) : null;
|
|
object = getPrototypeOf(object);
|
|
}
|
|
return null;
|
|
}
|
|
function isRegex(value) {
|
|
try {
|
|
regExpTest(value, "");
|
|
return true;
|
|
} catch (_unused) {
|
|
return false;
|
|
}
|
|
}
|
|
const html$1 = freeze([
|
|
"a",
|
|
"abbr",
|
|
"acronym",
|
|
"address",
|
|
"area",
|
|
"article",
|
|
"aside",
|
|
"audio",
|
|
"b",
|
|
"bdi",
|
|
"bdo",
|
|
"big",
|
|
"blink",
|
|
"blockquote",
|
|
"body",
|
|
"br",
|
|
"button",
|
|
"canvas",
|
|
"caption",
|
|
"center",
|
|
"cite",
|
|
"code",
|
|
"col",
|
|
"colgroup",
|
|
"content",
|
|
"data",
|
|
"datalist",
|
|
"dd",
|
|
"decorator",
|
|
"del",
|
|
"details",
|
|
"dfn",
|
|
"dialog",
|
|
"dir",
|
|
"div",
|
|
"dl",
|
|
"dt",
|
|
"element",
|
|
"em",
|
|
"fieldset",
|
|
"figcaption",
|
|
"figure",
|
|
"font",
|
|
"footer",
|
|
"form",
|
|
"h1",
|
|
"h2",
|
|
"h3",
|
|
"h4",
|
|
"h5",
|
|
"h6",
|
|
"head",
|
|
"header",
|
|
"hgroup",
|
|
"hr",
|
|
"html",
|
|
"i",
|
|
"img",
|
|
"input",
|
|
"ins",
|
|
"kbd",
|
|
"label",
|
|
"legend",
|
|
"li",
|
|
"main",
|
|
"map",
|
|
"mark",
|
|
"marquee",
|
|
"menu",
|
|
"menuitem",
|
|
"meter",
|
|
"nav",
|
|
"nobr",
|
|
"ol",
|
|
"optgroup",
|
|
"option",
|
|
"output",
|
|
"p",
|
|
"picture",
|
|
"pre",
|
|
"progress",
|
|
"q",
|
|
"rp",
|
|
"rt",
|
|
"ruby",
|
|
"s",
|
|
"samp",
|
|
"search",
|
|
"section",
|
|
"select",
|
|
"shadow",
|
|
"slot",
|
|
"small",
|
|
"source",
|
|
"spacer",
|
|
"span",
|
|
"strike",
|
|
"strong",
|
|
"style",
|
|
"sub",
|
|
"summary",
|
|
"sup",
|
|
"table",
|
|
"tbody",
|
|
"td",
|
|
"template",
|
|
"textarea",
|
|
"tfoot",
|
|
"th",
|
|
"thead",
|
|
"time",
|
|
"tr",
|
|
"track",
|
|
"tt",
|
|
"u",
|
|
"ul",
|
|
"var",
|
|
"video",
|
|
"wbr"
|
|
]);
|
|
const svg$1 = freeze([
|
|
"svg",
|
|
"a",
|
|
"altglyph",
|
|
"altglyphdef",
|
|
"altglyphitem",
|
|
"animatecolor",
|
|
"animatemotion",
|
|
"animatetransform",
|
|
"circle",
|
|
"clippath",
|
|
"defs",
|
|
"desc",
|
|
"ellipse",
|
|
"enterkeyhint",
|
|
"exportparts",
|
|
"filter",
|
|
"font",
|
|
"g",
|
|
"glyph",
|
|
"glyphref",
|
|
"hkern",
|
|
"image",
|
|
"inputmode",
|
|
"line",
|
|
"lineargradient",
|
|
"marker",
|
|
"mask",
|
|
"metadata",
|
|
"mpath",
|
|
"part",
|
|
"path",
|
|
"pattern",
|
|
"polygon",
|
|
"polyline",
|
|
"radialgradient",
|
|
"rect",
|
|
"stop",
|
|
"style",
|
|
"switch",
|
|
"symbol",
|
|
"text",
|
|
"textpath",
|
|
"title",
|
|
"tref",
|
|
"tspan",
|
|
"view",
|
|
"vkern"
|
|
]);
|
|
const svgFilters = freeze([
|
|
"feBlend",
|
|
"feColorMatrix",
|
|
"feComponentTransfer",
|
|
"feComposite",
|
|
"feConvolveMatrix",
|
|
"feDiffuseLighting",
|
|
"feDisplacementMap",
|
|
"feDistantLight",
|
|
"feDropShadow",
|
|
"feFlood",
|
|
"feFuncA",
|
|
"feFuncB",
|
|
"feFuncG",
|
|
"feFuncR",
|
|
"feGaussianBlur",
|
|
"feImage",
|
|
"feMerge",
|
|
"feMergeNode",
|
|
"feMorphology",
|
|
"feOffset",
|
|
"fePointLight",
|
|
"feSpecularLighting",
|
|
"feSpotLight",
|
|
"feTile",
|
|
"feTurbulence"
|
|
]);
|
|
const svgDisallowed = freeze([
|
|
"animate",
|
|
"color-profile",
|
|
"cursor",
|
|
"discard",
|
|
"font-face",
|
|
"font-face-format",
|
|
"font-face-name",
|
|
"font-face-src",
|
|
"font-face-uri",
|
|
"foreignobject",
|
|
"hatch",
|
|
"hatchpath",
|
|
"mesh",
|
|
"meshgradient",
|
|
"meshpatch",
|
|
"meshrow",
|
|
"missing-glyph",
|
|
"script",
|
|
"set",
|
|
"solidcolor",
|
|
"unknown",
|
|
"use"
|
|
]);
|
|
const mathMl$1 = freeze([
|
|
"math",
|
|
"menclose",
|
|
"merror",
|
|
"mfenced",
|
|
"mfrac",
|
|
"mglyph",
|
|
"mi",
|
|
"mlabeledtr",
|
|
"mmultiscripts",
|
|
"mn",
|
|
"mo",
|
|
"mover",
|
|
"mpadded",
|
|
"mphantom",
|
|
"mroot",
|
|
"mrow",
|
|
"ms",
|
|
"mspace",
|
|
"msqrt",
|
|
"mstyle",
|
|
"msub",
|
|
"msup",
|
|
"msubsup",
|
|
"mtable",
|
|
"mtd",
|
|
"mtext",
|
|
"mtr",
|
|
"munder",
|
|
"munderover",
|
|
"mprescripts"
|
|
]);
|
|
const mathMlDisallowed = freeze([
|
|
"maction",
|
|
"maligngroup",
|
|
"malignmark",
|
|
"mlongdiv",
|
|
"mscarries",
|
|
"mscarry",
|
|
"msgroup",
|
|
"mstack",
|
|
"msline",
|
|
"msrow",
|
|
"semantics",
|
|
"annotation",
|
|
"annotation-xml",
|
|
"mprescripts",
|
|
"none"
|
|
]);
|
|
const text = freeze(["#text"]);
|
|
const html = freeze([
|
|
"accept",
|
|
"action",
|
|
"align",
|
|
"alt",
|
|
"autocapitalize",
|
|
"autocomplete",
|
|
"autopictureinpicture",
|
|
"autoplay",
|
|
"background",
|
|
"bgcolor",
|
|
"border",
|
|
"capture",
|
|
"cellpadding",
|
|
"cellspacing",
|
|
"checked",
|
|
"cite",
|
|
"class",
|
|
"clear",
|
|
"color",
|
|
"cols",
|
|
"colspan",
|
|
"command",
|
|
"commandfor",
|
|
"controls",
|
|
"controlslist",
|
|
"coords",
|
|
"crossorigin",
|
|
"datetime",
|
|
"decoding",
|
|
"default",
|
|
"dir",
|
|
"disabled",
|
|
"disablepictureinpicture",
|
|
"disableremoteplayback",
|
|
"download",
|
|
"draggable",
|
|
"enctype",
|
|
"enterkeyhint",
|
|
"exportparts",
|
|
"face",
|
|
"for",
|
|
"headers",
|
|
"height",
|
|
"hidden",
|
|
"high",
|
|
"href",
|
|
"hreflang",
|
|
"id",
|
|
"inert",
|
|
"inputmode",
|
|
"integrity",
|
|
"ismap",
|
|
"kind",
|
|
"label",
|
|
"lang",
|
|
"list",
|
|
"loading",
|
|
"loop",
|
|
"low",
|
|
"max",
|
|
"maxlength",
|
|
"media",
|
|
"method",
|
|
"min",
|
|
"minlength",
|
|
"multiple",
|
|
"muted",
|
|
"name",
|
|
"nonce",
|
|
"noshade",
|
|
"novalidate",
|
|
"nowrap",
|
|
"open",
|
|
"optimum",
|
|
"part",
|
|
"pattern",
|
|
"placeholder",
|
|
"playsinline",
|
|
"popover",
|
|
"popovertarget",
|
|
"popovertargetaction",
|
|
"poster",
|
|
"preload",
|
|
"pubdate",
|
|
"radiogroup",
|
|
"readonly",
|
|
"rel",
|
|
"required",
|
|
"rev",
|
|
"reversed",
|
|
"role",
|
|
"rows",
|
|
"rowspan",
|
|
"spellcheck",
|
|
"scope",
|
|
"selected",
|
|
"shape",
|
|
"size",
|
|
"sizes",
|
|
"slot",
|
|
"span",
|
|
"srclang",
|
|
"start",
|
|
"src",
|
|
"srcset",
|
|
"step",
|
|
"style",
|
|
"summary",
|
|
"tabindex",
|
|
"title",
|
|
"translate",
|
|
"type",
|
|
"usemap",
|
|
"valign",
|
|
"value",
|
|
"width",
|
|
"wrap",
|
|
"xmlns"
|
|
]);
|
|
const svg = freeze([
|
|
"accent-height",
|
|
"accumulate",
|
|
"additive",
|
|
"alignment-baseline",
|
|
"amplitude",
|
|
"ascent",
|
|
"attributename",
|
|
"attributetype",
|
|
"azimuth",
|
|
"basefrequency",
|
|
"baseline-shift",
|
|
"begin",
|
|
"bias",
|
|
"by",
|
|
"class",
|
|
"clip",
|
|
"clippathunits",
|
|
"clip-path",
|
|
"clip-rule",
|
|
"color",
|
|
"color-interpolation",
|
|
"color-interpolation-filters",
|
|
"color-profile",
|
|
"color-rendering",
|
|
"cx",
|
|
"cy",
|
|
"d",
|
|
"dx",
|
|
"dy",
|
|
"diffuseconstant",
|
|
"direction",
|
|
"display",
|
|
"divisor",
|
|
"dominant-baseline",
|
|
"dur",
|
|
"edgemode",
|
|
"elevation",
|
|
"end",
|
|
"exponent",
|
|
"fill",
|
|
"fill-opacity",
|
|
"fill-rule",
|
|
"filter",
|
|
"filterunits",
|
|
"flood-color",
|
|
"flood-opacity",
|
|
"font-family",
|
|
"font-size",
|
|
"font-size-adjust",
|
|
"font-stretch",
|
|
"font-style",
|
|
"font-variant",
|
|
"font-weight",
|
|
"fx",
|
|
"fy",
|
|
"g1",
|
|
"g2",
|
|
"glyph-name",
|
|
"glyphref",
|
|
"gradientunits",
|
|
"gradienttransform",
|
|
"height",
|
|
"href",
|
|
"id",
|
|
"image-rendering",
|
|
"in",
|
|
"in2",
|
|
"intercept",
|
|
"k",
|
|
"k1",
|
|
"k2",
|
|
"k3",
|
|
"k4",
|
|
"kerning",
|
|
"keypoints",
|
|
"keysplines",
|
|
"keytimes",
|
|
"lang",
|
|
"lengthadjust",
|
|
"letter-spacing",
|
|
"kernelmatrix",
|
|
"kernelunitlength",
|
|
"lighting-color",
|
|
"local",
|
|
"marker-end",
|
|
"marker-mid",
|
|
"marker-start",
|
|
"markerheight",
|
|
"markerunits",
|
|
"markerwidth",
|
|
"maskcontentunits",
|
|
"maskunits",
|
|
"max",
|
|
"mask",
|
|
"mask-type",
|
|
"media",
|
|
"method",
|
|
"mode",
|
|
"min",
|
|
"name",
|
|
"numoctaves",
|
|
"offset",
|
|
"operator",
|
|
"opacity",
|
|
"order",
|
|
"orient",
|
|
"orientation",
|
|
"origin",
|
|
"overflow",
|
|
"paint-order",
|
|
"path",
|
|
"pathlength",
|
|
"patterncontentunits",
|
|
"patterntransform",
|
|
"patternunits",
|
|
"pointer-events",
|
|
"points",
|
|
"preservealpha",
|
|
"preserveaspectratio",
|
|
"primitiveunits",
|
|
"r",
|
|
"rx",
|
|
"ry",
|
|
"radius",
|
|
"refx",
|
|
"refy",
|
|
"repeatcount",
|
|
"repeatdur",
|
|
"restart",
|
|
"result",
|
|
"rotate",
|
|
"scale",
|
|
"seed",
|
|
"shape-rendering",
|
|
"slope",
|
|
"specularconstant",
|
|
"specularexponent",
|
|
"spreadmethod",
|
|
"startoffset",
|
|
"stddeviation",
|
|
"stitchtiles",
|
|
"stop-color",
|
|
"stop-opacity",
|
|
"stroke-dasharray",
|
|
"stroke-dashoffset",
|
|
"stroke-linecap",
|
|
"stroke-linejoin",
|
|
"stroke-miterlimit",
|
|
"stroke-opacity",
|
|
"stroke",
|
|
"stroke-width",
|
|
"style",
|
|
"surfacescale",
|
|
"systemlanguage",
|
|
"tabindex",
|
|
"tablevalues",
|
|
"targetx",
|
|
"targety",
|
|
"transform",
|
|
"transform-origin",
|
|
"text-anchor",
|
|
"text-decoration",
|
|
"text-orientation",
|
|
"text-rendering",
|
|
"textlength",
|
|
"type",
|
|
"u1",
|
|
"u2",
|
|
"unicode",
|
|
"values",
|
|
"vector-effect",
|
|
"viewbox",
|
|
"visibility",
|
|
"version",
|
|
"vert-adv-y",
|
|
"vert-origin-x",
|
|
"vert-origin-y",
|
|
"width",
|
|
"word-spacing",
|
|
"wrap",
|
|
"writing-mode",
|
|
"xchannelselector",
|
|
"ychannelselector",
|
|
"x",
|
|
"x1",
|
|
"x2",
|
|
"xmlns",
|
|
"y",
|
|
"y1",
|
|
"y2",
|
|
"z",
|
|
"zoomandpan"
|
|
]);
|
|
const mathMl = freeze([
|
|
"accent",
|
|
"accentunder",
|
|
"align",
|
|
"bevelled",
|
|
"close",
|
|
"columnalign",
|
|
"columnlines",
|
|
"columnspacing",
|
|
"columnspan",
|
|
"denomalign",
|
|
"depth",
|
|
"dir",
|
|
"display",
|
|
"displaystyle",
|
|
"encoding",
|
|
"fence",
|
|
"frame",
|
|
"height",
|
|
"href",
|
|
"id",
|
|
"largeop",
|
|
"length",
|
|
"linethickness",
|
|
"lquote",
|
|
"lspace",
|
|
"mathbackground",
|
|
"mathcolor",
|
|
"mathsize",
|
|
"mathvariant",
|
|
"maxsize",
|
|
"minsize",
|
|
"movablelimits",
|
|
"notation",
|
|
"numalign",
|
|
"open",
|
|
"rowalign",
|
|
"rowlines",
|
|
"rowspacing",
|
|
"rowspan",
|
|
"rspace",
|
|
"rquote",
|
|
"scriptlevel",
|
|
"scriptminsize",
|
|
"scriptsizemultiplier",
|
|
"selection",
|
|
"separator",
|
|
"separators",
|
|
"stretchy",
|
|
"subscriptshift",
|
|
"supscriptshift",
|
|
"symmetric",
|
|
"voffset",
|
|
"width",
|
|
"xmlns"
|
|
]);
|
|
const xml = freeze([
|
|
"xlink:href",
|
|
"xml:id",
|
|
"xlink:title",
|
|
"xml:space",
|
|
"xmlns:xlink"
|
|
]);
|
|
const MUSTACHE_EXPR = seal(/{{[\w\W]*|^[\w\W]*}}/g);
|
|
const ERB_EXPR = seal(/<%[\w\W]*|^[\w\W]*%>/g);
|
|
const TMPLIT_EXPR = seal(/\${[\w\W]*/g);
|
|
const DATA_ATTR = seal(/^data-[\-\w.\u00B7-\uFFFF]+$/);
|
|
const ARIA_ATTR = seal(/^aria-[\-\w]+$/);
|
|
const IS_ALLOWED_URI = seal(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|matrix):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i);
|
|
const IS_SCRIPT_OR_DATA = seal(/^(?:\w+script|data):/i);
|
|
const ATTR_WHITESPACE = seal(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g);
|
|
const DOCTYPE_NAME = seal(/^html$/i);
|
|
const CUSTOM_ELEMENT = seal(/^[a-z][.\w]*(-[.\w]+)+$/i);
|
|
const ELEMENT_MARKUP_PROBE = seal(/<[/\w!]/g);
|
|
const COMMENT_MARKUP_PROBE = seal(/<[/\w]/g);
|
|
const FALLBACK_TAG_CLOSE = seal(/<\/no(script|embed|frames)/i);
|
|
const SELF_CLOSING_TAG = seal(/\/>/i);
|
|
const NODE_TYPE = {
|
|
element: 1,
|
|
attribute: 2,
|
|
text: 3,
|
|
cdataSection: 4,
|
|
entityReference: 5,
|
|
entityNode: 6,
|
|
processingInstruction: 7,
|
|
comment: 8,
|
|
document: 9,
|
|
documentType: 10,
|
|
documentFragment: 11,
|
|
notation: 12
|
|
};
|
|
const LITERAL_TEXT_ELEMENTS = freeze(addToSet({}, [
|
|
"style",
|
|
"script",
|
|
"xmp",
|
|
"iframe",
|
|
"noembed",
|
|
"noframes",
|
|
"plaintext",
|
|
"noscript"
|
|
]));
|
|
const getGlobal = function getGlobal() {
|
|
return typeof window === "undefined" ? null : window;
|
|
};
|
|
/**
|
|
* Creates a no-op policy for internal use only.
|
|
* Don't export this function outside this module!
|
|
* @param trustedTypes The policy factory.
|
|
* @param purifyHostElement The Script element used to load DOMPurify (to determine policy name suffix).
|
|
* @return The policy created (or null, if Trusted Types
|
|
* are not supported or creating the policy failed).
|
|
*/
|
|
const _createTrustedTypesPolicy = function _createTrustedTypesPolicy(trustedTypes, purifyHostElement) {
|
|
if (typeof trustedTypes !== "object" || typeof trustedTypes.createPolicy !== "function") return null;
|
|
let suffix = null;
|
|
const ATTR_NAME = "data-tt-policy-suffix";
|
|
if (purifyHostElement && purifyHostElement.hasAttribute(ATTR_NAME)) suffix = purifyHostElement.getAttribute(ATTR_NAME);
|
|
const policyName = "dompurify" + (suffix ? "#" + suffix : "");
|
|
try {
|
|
return trustedTypes.createPolicy(policyName, {
|
|
createHTML(html) {
|
|
return html;
|
|
},
|
|
createScriptURL(scriptUrl) {
|
|
return scriptUrl;
|
|
}
|
|
});
|
|
} catch (_) {
|
|
console.warn("TrustedTypes policy " + policyName + " could not be created.");
|
|
return null;
|
|
}
|
|
};
|
|
const _createHooksMap = function _createHooksMap() {
|
|
return {
|
|
afterSanitizeAttributes: [],
|
|
afterSanitizeElements: [],
|
|
afterSanitizeShadowDOM: [],
|
|
beforeSanitizeAttributes: [],
|
|
beforeSanitizeElements: [],
|
|
beforeSanitizeShadowDOM: [],
|
|
uponSanitizeAttribute: [],
|
|
uponSanitizeElement: [],
|
|
uponSanitizeShadowNode: []
|
|
};
|
|
};
|
|
/**
|
|
* Resolve a set-valued configuration option: a fresh set built from
|
|
* cfg[key] when it is an own array property (seeded with a clone of
|
|
* options.base when given, case-normalized via options.transform),
|
|
* the fallback set otherwise.
|
|
*
|
|
* @param cfg the cloned, prototype-free configuration object
|
|
* @param key the configuration property to read
|
|
* @param fallback the set to use when the option is absent or not an array
|
|
* @param options transform and optional base set to merge into
|
|
* @returns the resolved set
|
|
*/
|
|
const _resolveSetOption = function _resolveSetOption(cfg, key, fallback, options) {
|
|
return objectHasOwnProperty(cfg, key) && arrayIsArray(cfg[key]) ? addToSet(options.base ? clone(options.base) : {}, cfg[key], options.transform) : fallback;
|
|
};
|
|
/**
|
|
* Resolve an object-valued configuration option: a prototype-free clone
|
|
* of cfg[key] when it is an own, truthy object property, else a fresh
|
|
* fallback built by makeFallback (fresh on every parse, so a previous
|
|
* parse can never leak state into the next one).
|
|
*
|
|
* @param cfg the cloned, prototype-free configuration object
|
|
* @param key the configuration property to read
|
|
* @param makeFallback builds the fallback value when the option is absent
|
|
* @returns the resolved object
|
|
*/
|
|
const _resolveObjectOption = function _resolveObjectOption(cfg, key, makeFallback) {
|
|
const value = objectHasOwnProperty(cfg, key) ? cfg[key] : void 0;
|
|
return value && typeof value === "object" ? clone(value) : makeFallback();
|
|
};
|
|
function createDOMPurify() {
|
|
let window = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : getGlobal();
|
|
const DOMPurify = (root) => createDOMPurify(root);
|
|
DOMPurify.version = "3.4.17";
|
|
DOMPurify.removed = [];
|
|
if (!window || !window.document || window.document.nodeType !== NODE_TYPE.document || !window.Element) {
|
|
DOMPurify.isSupported = false;
|
|
return DOMPurify;
|
|
}
|
|
let document = window.document;
|
|
const originalDocument = document;
|
|
const currentScript = originalDocument.currentScript;
|
|
window.DocumentFragment;
|
|
const HTMLTemplateElement = window.HTMLTemplateElement, Node = window.Node, Element = window.Element, NodeFilter = window.NodeFilter;
|
|
window.NamedNodeMap === void 0 && (window.NamedNodeMap || window.MozNamedAttrMap);
|
|
window.HTMLFormElement;
|
|
const DOMParser = window.DOMParser, trustedTypes = window.trustedTypes;
|
|
const ElementPrototype = Element.prototype;
|
|
const cloneNode = lookupGetter(ElementPrototype, "cloneNode");
|
|
const remove = lookupGetter(ElementPrototype, "remove");
|
|
const removeAttributeNode = lookupGetter(ElementPrototype, "removeAttributeNode");
|
|
const getNextSibling = lookupGetter(ElementPrototype, "nextSibling");
|
|
const getChildNodes = lookupGetter(ElementPrototype, "childNodes");
|
|
const getParentNode = lookupGetter(ElementPrototype, "parentNode");
|
|
const getShadowRoot = lookupGetter(ElementPrototype, "shadowRoot");
|
|
const getAttributes = lookupGetter(ElementPrototype, "attributes");
|
|
const getNodeType = Node && Node.prototype ? lookupGetter(Node.prototype, "nodeType") : null;
|
|
const getNodeName = Node && Node.prototype ? lookupGetter(Node.prototype, "nodeName") : null;
|
|
const getOwnerDocument = Node && Node.prototype ? lookupGetter(Node.prototype, "ownerDocument") : null;
|
|
const _readNodeType = function _readNodeType(node) {
|
|
return getNodeType ? getNodeType(node) : node.nodeType;
|
|
};
|
|
const _readNodeName = function _readNodeName(node) {
|
|
return getNodeName ? getNodeName(node) : node.nodeName;
|
|
};
|
|
const getNamespaceURI = lookupAccessor(ElementPrototype, "namespaceURI");
|
|
const _readNamespaceURI = function _readNamespaceURI(element) {
|
|
return getNamespaceURI ? getNamespaceURI(element) : element.namespaceURI;
|
|
};
|
|
if (typeof HTMLTemplateElement === "function") {
|
|
const template = document.createElement("template");
|
|
if (template.content && template.content.ownerDocument) document = template.content.ownerDocument;
|
|
}
|
|
let trustedTypesPolicy;
|
|
let emptyHTML = "";
|
|
let defaultTrustedTypesPolicy;
|
|
let defaultTrustedTypesPolicyResolved = false;
|
|
let IN_TRUSTED_TYPES_POLICY = 0;
|
|
const _assertNotInTrustedTypesPolicy = function _assertNotInTrustedTypesPolicy() {
|
|
if (IN_TRUSTED_TYPES_POLICY > 0) throw typeErrorCreate("A configured TRUSTED_TYPES_POLICY callback (createHTML or createScriptURL) must not call DOMPurify.sanitize, as that causes infinite recursion. Do not pass a policy whose callbacks wrap DOMPurify as TRUSTED_TYPES_POLICY; see the \"DOMPurify and Trusted Types\" section of the README.");
|
|
};
|
|
const _createTrustedHTML = function _createTrustedHTML(html) {
|
|
_assertNotInTrustedTypesPolicy();
|
|
IN_TRUSTED_TYPES_POLICY++;
|
|
try {
|
|
return trustedTypesPolicy.createHTML(html);
|
|
} finally {
|
|
IN_TRUSTED_TYPES_POLICY--;
|
|
}
|
|
};
|
|
const _createTrustedScriptURL = function _createTrustedScriptURL(scriptUrl) {
|
|
_assertNotInTrustedTypesPolicy();
|
|
IN_TRUSTED_TYPES_POLICY++;
|
|
try {
|
|
return trustedTypesPolicy.createScriptURL(scriptUrl);
|
|
} finally {
|
|
IN_TRUSTED_TYPES_POLICY--;
|
|
}
|
|
};
|
|
const _getDefaultTrustedTypesPolicy = function _getDefaultTrustedTypesPolicy() {
|
|
if (!defaultTrustedTypesPolicyResolved) {
|
|
defaultTrustedTypesPolicy = _createTrustedTypesPolicy(trustedTypes, currentScript);
|
|
defaultTrustedTypesPolicyResolved = true;
|
|
}
|
|
return defaultTrustedTypesPolicy;
|
|
};
|
|
const _document = document, implementation = _document.implementation, createNodeIterator = _document.createNodeIterator, createDocumentFragment = _document.createDocumentFragment, getElementsByTagName = _document.getElementsByTagName;
|
|
const importNode = originalDocument.importNode;
|
|
let hooks = _createHooksMap();
|
|
/**
|
|
* Expose whether this browser supports running the full DOMPurify.
|
|
*/
|
|
DOMPurify.isSupported = typeof entries === "function" && typeof getParentNode === "function" && implementation && implementation.createHTMLDocument !== void 0;
|
|
const MUSTACHE_EXPR$1 = MUSTACHE_EXPR, ERB_EXPR$1 = ERB_EXPR, TMPLIT_EXPR$1 = TMPLIT_EXPR, DATA_ATTR$1 = DATA_ATTR, ARIA_ATTR$1 = ARIA_ATTR, IS_SCRIPT_OR_DATA$1 = IS_SCRIPT_OR_DATA, ATTR_WHITESPACE$1 = ATTR_WHITESPACE, CUSTOM_ELEMENT$1 = CUSTOM_ELEMENT;
|
|
let IS_ALLOWED_URI$1 = IS_ALLOWED_URI;
|
|
/**
|
|
* We consider the elements and attributes below to be safe. Ideally
|
|
* don't add any new ones but feel free to remove unwanted ones.
|
|
*/
|
|
let ALLOWED_TAGS = null;
|
|
const DEFAULT_ALLOWED_TAGS = addToSet({}, [
|
|
...html$1,
|
|
...svg$1,
|
|
...svgFilters,
|
|
...mathMl$1,
|
|
...text
|
|
]);
|
|
let ALLOWED_ATTR = null;
|
|
const DEFAULT_ALLOWED_ATTR = addToSet({}, [
|
|
...html,
|
|
...svg,
|
|
...mathMl,
|
|
...xml
|
|
]);
|
|
let CUSTOM_ELEMENT_HANDLING = Object.seal(create(null, {
|
|
tagNameCheck: {
|
|
writable: true,
|
|
configurable: false,
|
|
enumerable: true,
|
|
value: null
|
|
},
|
|
attributeNameCheck: {
|
|
writable: true,
|
|
configurable: false,
|
|
enumerable: true,
|
|
value: null
|
|
},
|
|
allowCustomizedBuiltInElements: {
|
|
writable: true,
|
|
configurable: false,
|
|
enumerable: true,
|
|
value: false
|
|
}
|
|
}));
|
|
let FORBID_TAGS = null;
|
|
let FORBID_ATTR = null;
|
|
const EXTRA_ELEMENT_HANDLING = Object.seal(create(null, {
|
|
tagCheck: {
|
|
writable: true,
|
|
configurable: false,
|
|
enumerable: true,
|
|
value: null
|
|
},
|
|
attributeCheck: {
|
|
writable: true,
|
|
configurable: false,
|
|
enumerable: true,
|
|
value: null
|
|
}
|
|
}));
|
|
let ALLOW_ARIA_ATTR = true;
|
|
let ALLOW_DATA_ATTR = true;
|
|
let ALLOW_UNKNOWN_PROTOCOLS = false;
|
|
let ALLOW_SELF_CLOSE_IN_ATTR = true;
|
|
let SAFE_FOR_TEMPLATES = false;
|
|
let SAFE_FOR_XML = true;
|
|
let WHOLE_DOCUMENT = false;
|
|
let SET_CONFIG = false;
|
|
let SET_CONFIG_ALLOWED_TAGS = null;
|
|
let SET_CONFIG_ALLOWED_ATTR = null;
|
|
let FORCE_BODY = false;
|
|
let RETURN_DOM = false;
|
|
let RETURN_DOM_FRAGMENT = false;
|
|
let RETURN_TRUSTED_TYPE = false;
|
|
let SANITIZE_DOM = true;
|
|
let SANITIZE_NAMED_PROPS = false;
|
|
const SANITIZE_NAMED_PROPS_PREFIX = "user-content-";
|
|
let KEEP_CONTENT = true;
|
|
let IN_PLACE = false;
|
|
let IN_PLACE_VISITED = null;
|
|
let USE_PROFILES = {};
|
|
let FORBID_CONTENTS = null;
|
|
const DEFAULT_FORBID_CONTENTS = addToSet({}, [
|
|
"annotation-xml",
|
|
"audio",
|
|
"colgroup",
|
|
"desc",
|
|
"foreignobject",
|
|
"head",
|
|
"iframe",
|
|
"math",
|
|
"mi",
|
|
"mn",
|
|
"mo",
|
|
"ms",
|
|
"mtext",
|
|
"noembed",
|
|
"noframes",
|
|
"noscript",
|
|
"plaintext",
|
|
"script",
|
|
"selectedcontent",
|
|
"style",
|
|
"svg",
|
|
"template",
|
|
"thead",
|
|
"title",
|
|
"video",
|
|
"xmp"
|
|
]);
|
|
let DATA_URI_TAGS = null;
|
|
const DEFAULT_DATA_URI_TAGS = addToSet({}, [
|
|
"audio",
|
|
"video",
|
|
"img",
|
|
"source",
|
|
"image",
|
|
"track"
|
|
]);
|
|
let URI_SAFE_ATTRIBUTES = null;
|
|
const DEFAULT_URI_SAFE_ATTRIBUTES = addToSet({}, [
|
|
"alt",
|
|
"class",
|
|
"for",
|
|
"id",
|
|
"label",
|
|
"name",
|
|
"pattern",
|
|
"placeholder",
|
|
"role",
|
|
"summary",
|
|
"title",
|
|
"value",
|
|
"style",
|
|
"xmlns"
|
|
]);
|
|
const MATHML_NAMESPACE = "http://www.w3.org/1998/Math/MathML";
|
|
const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
|
|
const HTML_NAMESPACE = "http://www.w3.org/1999/xhtml";
|
|
let NAMESPACE = HTML_NAMESPACE;
|
|
let IS_EMPTY_INPUT = false;
|
|
let ALLOWED_NAMESPACES = null;
|
|
const DEFAULT_ALLOWED_NAMESPACES = addToSet({}, [
|
|
MATHML_NAMESPACE,
|
|
SVG_NAMESPACE,
|
|
HTML_NAMESPACE
|
|
], stringToString);
|
|
const DEFAULT_MATHML_TEXT_INTEGRATION_POINTS = freeze([
|
|
"mi",
|
|
"mo",
|
|
"mn",
|
|
"ms",
|
|
"mtext"
|
|
]);
|
|
let MATHML_TEXT_INTEGRATION_POINTS = addToSet({}, DEFAULT_MATHML_TEXT_INTEGRATION_POINTS);
|
|
const DEFAULT_HTML_INTEGRATION_POINTS = freeze(["annotation-xml"]);
|
|
let HTML_INTEGRATION_POINTS = addToSet({}, DEFAULT_HTML_INTEGRATION_POINTS);
|
|
const COMMON_SVG_AND_HTML_ELEMENTS = addToSet({}, [
|
|
"title",
|
|
"style",
|
|
"font",
|
|
"a",
|
|
"script"
|
|
]);
|
|
let PARSER_MEDIA_TYPE = null;
|
|
const SUPPORTED_PARSER_MEDIA_TYPES = ["application/xhtml+xml", "text/html"];
|
|
const DEFAULT_PARSER_MEDIA_TYPE = "text/html";
|
|
let transformCaseFunc = null;
|
|
let CONFIG = null;
|
|
const formElement = document.createElement("form");
|
|
const isRegexOrFunction = function isRegexOrFunction(testValue) {
|
|
return testValue instanceof RegExp || testValue instanceof Function;
|
|
};
|
|
/**
|
|
* _parseConfig
|
|
*
|
|
* @param cfg optional config literal
|
|
*/
|
|
const _parseConfig = function _parseConfig() {
|
|
let cfg = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : {};
|
|
if (CONFIG && CONFIG === cfg) return;
|
|
if (!cfg || typeof cfg !== "object") cfg = {};
|
|
cfg = clone(cfg);
|
|
PARSER_MEDIA_TYPE = SUPPORTED_PARSER_MEDIA_TYPES.indexOf(cfg.PARSER_MEDIA_TYPE) === -1 ? DEFAULT_PARSER_MEDIA_TYPE : cfg.PARSER_MEDIA_TYPE;
|
|
transformCaseFunc = PARSER_MEDIA_TYPE === "application/xhtml+xml" ? stringToString : stringToLowerCase;
|
|
ALLOWED_TAGS = _resolveSetOption(cfg, "ALLOWED_TAGS", DEFAULT_ALLOWED_TAGS, { transform: transformCaseFunc });
|
|
ALLOWED_ATTR = _resolveSetOption(cfg, "ALLOWED_ATTR", DEFAULT_ALLOWED_ATTR, { transform: transformCaseFunc });
|
|
ALLOWED_NAMESPACES = _resolveSetOption(cfg, "ALLOWED_NAMESPACES", DEFAULT_ALLOWED_NAMESPACES, { transform: stringToString });
|
|
URI_SAFE_ATTRIBUTES = _resolveSetOption(cfg, "ADD_URI_SAFE_ATTR", DEFAULT_URI_SAFE_ATTRIBUTES, {
|
|
transform: transformCaseFunc,
|
|
base: DEFAULT_URI_SAFE_ATTRIBUTES
|
|
});
|
|
DATA_URI_TAGS = _resolveSetOption(cfg, "ADD_DATA_URI_TAGS", DEFAULT_DATA_URI_TAGS, {
|
|
transform: transformCaseFunc,
|
|
base: DEFAULT_DATA_URI_TAGS
|
|
});
|
|
FORBID_CONTENTS = _resolveSetOption(cfg, "FORBID_CONTENTS", DEFAULT_FORBID_CONTENTS, { transform: transformCaseFunc });
|
|
FORBID_TAGS = _resolveSetOption(cfg, "FORBID_TAGS", clone({}), { transform: transformCaseFunc });
|
|
FORBID_ATTR = _resolveSetOption(cfg, "FORBID_ATTR", clone({}), { transform: transformCaseFunc });
|
|
USE_PROFILES = objectHasOwnProperty(cfg, "USE_PROFILES") ? cfg.USE_PROFILES && typeof cfg.USE_PROFILES === "object" ? clone(cfg.USE_PROFILES) : cfg.USE_PROFILES : false;
|
|
ALLOW_ARIA_ATTR = cfg.ALLOW_ARIA_ATTR !== false;
|
|
ALLOW_DATA_ATTR = cfg.ALLOW_DATA_ATTR !== false;
|
|
ALLOW_UNKNOWN_PROTOCOLS = cfg.ALLOW_UNKNOWN_PROTOCOLS || false;
|
|
ALLOW_SELF_CLOSE_IN_ATTR = cfg.ALLOW_SELF_CLOSE_IN_ATTR !== false;
|
|
SAFE_FOR_TEMPLATES = cfg.SAFE_FOR_TEMPLATES || false;
|
|
SAFE_FOR_XML = cfg.SAFE_FOR_XML !== false;
|
|
WHOLE_DOCUMENT = cfg.WHOLE_DOCUMENT || false;
|
|
RETURN_DOM = cfg.RETURN_DOM || false;
|
|
RETURN_DOM_FRAGMENT = cfg.RETURN_DOM_FRAGMENT || false;
|
|
RETURN_TRUSTED_TYPE = cfg.RETURN_TRUSTED_TYPE || false;
|
|
FORCE_BODY = cfg.FORCE_BODY || false;
|
|
SANITIZE_DOM = cfg.SANITIZE_DOM !== false;
|
|
SANITIZE_NAMED_PROPS = cfg.SANITIZE_NAMED_PROPS || false;
|
|
KEEP_CONTENT = cfg.KEEP_CONTENT !== false;
|
|
IN_PLACE = cfg.IN_PLACE || false;
|
|
IS_ALLOWED_URI$1 = isRegex(cfg.ALLOWED_URI_REGEXP) ? cfg.ALLOWED_URI_REGEXP : IS_ALLOWED_URI;
|
|
NAMESPACE = typeof cfg.NAMESPACE === "string" ? cfg.NAMESPACE : HTML_NAMESPACE;
|
|
MATHML_TEXT_INTEGRATION_POINTS = _resolveObjectOption(cfg, "MATHML_TEXT_INTEGRATION_POINTS", () => addToSet({}, DEFAULT_MATHML_TEXT_INTEGRATION_POINTS));
|
|
HTML_INTEGRATION_POINTS = _resolveObjectOption(cfg, "HTML_INTEGRATION_POINTS", () => addToSet({}, DEFAULT_HTML_INTEGRATION_POINTS));
|
|
const customElementHandling = _resolveObjectOption(cfg, "CUSTOM_ELEMENT_HANDLING", () => create(null));
|
|
CUSTOM_ELEMENT_HANDLING = create(null);
|
|
if (objectHasOwnProperty(customElementHandling, "tagNameCheck") && isRegexOrFunction(customElementHandling.tagNameCheck)) CUSTOM_ELEMENT_HANDLING.tagNameCheck = customElementHandling.tagNameCheck;
|
|
if (objectHasOwnProperty(customElementHandling, "attributeNameCheck") && isRegexOrFunction(customElementHandling.attributeNameCheck)) CUSTOM_ELEMENT_HANDLING.attributeNameCheck = customElementHandling.attributeNameCheck;
|
|
if (objectHasOwnProperty(customElementHandling, "allowCustomizedBuiltInElements") && typeof customElementHandling.allowCustomizedBuiltInElements === "boolean") CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements = customElementHandling.allowCustomizedBuiltInElements;
|
|
seal(CUSTOM_ELEMENT_HANDLING);
|
|
if (SAFE_FOR_TEMPLATES) ALLOW_DATA_ATTR = false;
|
|
if (RETURN_DOM_FRAGMENT) RETURN_DOM = true;
|
|
if (USE_PROFILES) {
|
|
ALLOWED_TAGS = addToSet({}, text);
|
|
ALLOWED_ATTR = create(null);
|
|
if (USE_PROFILES.html === true) {
|
|
addToSet(ALLOWED_TAGS, html$1);
|
|
addToSet(ALLOWED_ATTR, html);
|
|
}
|
|
if (USE_PROFILES.svg === true) {
|
|
addToSet(ALLOWED_TAGS, svg$1);
|
|
addToSet(ALLOWED_ATTR, svg);
|
|
addToSet(ALLOWED_ATTR, xml);
|
|
}
|
|
if (USE_PROFILES.svgFilters === true) {
|
|
addToSet(ALLOWED_TAGS, svgFilters);
|
|
addToSet(ALLOWED_ATTR, svg);
|
|
addToSet(ALLOWED_ATTR, xml);
|
|
}
|
|
if (USE_PROFILES.mathMl === true) {
|
|
addToSet(ALLOWED_TAGS, mathMl$1);
|
|
addToSet(ALLOWED_ATTR, mathMl);
|
|
addToSet(ALLOWED_ATTR, xml);
|
|
}
|
|
}
|
|
EXTRA_ELEMENT_HANDLING.tagCheck = null;
|
|
EXTRA_ELEMENT_HANDLING.attributeCheck = null;
|
|
if (objectHasOwnProperty(cfg, "ADD_TAGS")) {
|
|
if (typeof cfg.ADD_TAGS === "function") EXTRA_ELEMENT_HANDLING.tagCheck = cfg.ADD_TAGS;
|
|
else if (arrayIsArray(cfg.ADD_TAGS)) {
|
|
if (ALLOWED_TAGS === DEFAULT_ALLOWED_TAGS) ALLOWED_TAGS = clone(ALLOWED_TAGS);
|
|
addToSet(ALLOWED_TAGS, cfg.ADD_TAGS, transformCaseFunc);
|
|
}
|
|
}
|
|
if (objectHasOwnProperty(cfg, "ADD_ATTR")) {
|
|
if (typeof cfg.ADD_ATTR === "function") EXTRA_ELEMENT_HANDLING.attributeCheck = cfg.ADD_ATTR;
|
|
else if (arrayIsArray(cfg.ADD_ATTR)) {
|
|
if (ALLOWED_ATTR === DEFAULT_ALLOWED_ATTR) ALLOWED_ATTR = clone(ALLOWED_ATTR);
|
|
addToSet(ALLOWED_ATTR, cfg.ADD_ATTR, transformCaseFunc);
|
|
}
|
|
}
|
|
if (objectHasOwnProperty(cfg, "ADD_FORBID_CONTENTS") && arrayIsArray(cfg.ADD_FORBID_CONTENTS)) {
|
|
if (FORBID_CONTENTS === DEFAULT_FORBID_CONTENTS) FORBID_CONTENTS = clone(FORBID_CONTENTS);
|
|
addToSet(FORBID_CONTENTS, cfg.ADD_FORBID_CONTENTS, transformCaseFunc);
|
|
}
|
|
if (KEEP_CONTENT) ALLOWED_TAGS["#text"] = true;
|
|
if (WHOLE_DOCUMENT) addToSet(ALLOWED_TAGS, [
|
|
"html",
|
|
"head",
|
|
"body"
|
|
]);
|
|
if (ALLOWED_TAGS.table) {
|
|
addToSet(ALLOWED_TAGS, ["tbody"]);
|
|
delete FORBID_TAGS.tbody;
|
|
}
|
|
if (cfg.TRUSTED_TYPES_POLICY) {
|
|
if (typeof cfg.TRUSTED_TYPES_POLICY.createHTML !== "function") throw typeErrorCreate("TRUSTED_TYPES_POLICY configuration option must provide a \"createHTML\" hook.");
|
|
if (typeof cfg.TRUSTED_TYPES_POLICY.createScriptURL !== "function") throw typeErrorCreate("TRUSTED_TYPES_POLICY configuration option must provide a \"createScriptURL\" hook.");
|
|
const previousTrustedTypesPolicy = trustedTypesPolicy;
|
|
trustedTypesPolicy = cfg.TRUSTED_TYPES_POLICY;
|
|
try {
|
|
emptyHTML = _createTrustedHTML("");
|
|
} catch (error) {
|
|
trustedTypesPolicy = previousTrustedTypesPolicy;
|
|
throw error;
|
|
}
|
|
} else if (cfg.TRUSTED_TYPES_POLICY === null) {
|
|
trustedTypesPolicy = void 0;
|
|
emptyHTML = "";
|
|
} else {
|
|
if (trustedTypesPolicy === void 0) trustedTypesPolicy = _getDefaultTrustedTypesPolicy();
|
|
if (trustedTypesPolicy && typeof emptyHTML === "string") emptyHTML = _createTrustedHTML("");
|
|
}
|
|
if (freeze) freeze(cfg);
|
|
CONFIG = cfg;
|
|
};
|
|
const ALL_SVG_TAGS = addToSet({}, [
|
|
...svg$1,
|
|
...svgFilters,
|
|
...svgDisallowed
|
|
]);
|
|
const ALL_MATHML_TAGS = addToSet({}, [...mathMl$1, ...mathMlDisallowed]);
|
|
/**
|
|
* Namespace rules for an element in the SVG namespace.
|
|
*
|
|
* @param tagName the element's lowercase tag name
|
|
* @param parent the (possibly simulated) parent node
|
|
* @param parentTagName the parent's lowercase tag name
|
|
* @returns true if a spec-compliant parser could produce this element
|
|
*/
|
|
const _checkSvgNamespace = function _checkSvgNamespace(tagName, parent, parentTagName) {
|
|
if (parent.namespaceURI === HTML_NAMESPACE) return tagName === "svg";
|
|
if (parent.namespaceURI === MATHML_NAMESPACE) return tagName === "svg" && (parentTagName === "annotation-xml" || MATHML_TEXT_INTEGRATION_POINTS[parentTagName]);
|
|
return Boolean(ALL_SVG_TAGS[tagName]);
|
|
};
|
|
/**
|
|
* Namespace rules for an element in the MathML namespace.
|
|
*
|
|
* @param tagName the element's lowercase tag name
|
|
* @param parent the (possibly simulated) parent node
|
|
* @param parentTagName the parent's lowercase tag name
|
|
* @returns true if a spec-compliant parser could produce this element
|
|
*/
|
|
const _checkMathMlNamespace = function _checkMathMlNamespace(tagName, parent, parentTagName) {
|
|
if (parent.namespaceURI === HTML_NAMESPACE) return tagName === "math";
|
|
if (parent.namespaceURI === SVG_NAMESPACE) return tagName === "math" && HTML_INTEGRATION_POINTS[parentTagName];
|
|
return Boolean(ALL_MATHML_TAGS[tagName]);
|
|
};
|
|
/**
|
|
* Namespace rules for an element in the HTML namespace.
|
|
*
|
|
* @param tagName the element's lowercase tag name
|
|
* @param parent the (possibly simulated) parent node
|
|
* @param parentTagName the parent's lowercase tag name
|
|
* @returns true if a spec-compliant parser could produce this element
|
|
*/
|
|
const _checkHtmlNamespace = function _checkHtmlNamespace(tagName, parent, parentTagName) {
|
|
if (parent.namespaceURI === SVG_NAMESPACE && !HTML_INTEGRATION_POINTS[parentTagName]) return false;
|
|
if (parent.namespaceURI === MATHML_NAMESPACE && !MATHML_TEXT_INTEGRATION_POINTS[parentTagName]) return false;
|
|
return !ALL_MATHML_TAGS[tagName] && (COMMON_SVG_AND_HTML_ELEMENTS[tagName] || !ALL_SVG_TAGS[tagName]);
|
|
};
|
|
/**
|
|
* @param element a DOM element whose namespace is being checked
|
|
* @returns Return false if the element has a
|
|
* namespace that a spec-compliant parser would never
|
|
* return. Return true otherwise.
|
|
*/
|
|
const _checkValidNamespace = function _checkValidNamespace(element) {
|
|
const elementNamespace = _readNamespaceURI(element);
|
|
const tagName = stringToLowerCase(_readNodeName(element));
|
|
const parentNode = getParentNode(element);
|
|
let parent;
|
|
let parentTagName;
|
|
if (parentNode && _readNodeType(parentNode) === NODE_TYPE.element) {
|
|
parent = { namespaceURI: _readNamespaceURI(parentNode) };
|
|
parentTagName = stringToLowerCase(_readNodeName(parentNode));
|
|
} else {
|
|
parent = { namespaceURI: NAMESPACE };
|
|
parentTagName = "template";
|
|
}
|
|
if (!ALLOWED_NAMESPACES[elementNamespace]) return false;
|
|
if (elementNamespace === SVG_NAMESPACE) return _checkSvgNamespace(tagName, parent, parentTagName);
|
|
if (elementNamespace === MATHML_NAMESPACE) return _checkMathMlNamespace(tagName, parent, parentTagName);
|
|
if (elementNamespace === HTML_NAMESPACE) return _checkHtmlNamespace(tagName, parent, parentTagName);
|
|
if (PARSER_MEDIA_TYPE === "application/xhtml+xml" && ALLOWED_NAMESPACES[elementNamespace]) return true;
|
|
return false;
|
|
};
|
|
/**
|
|
* _forceRemove
|
|
*
|
|
* @param node a DOM node
|
|
*/
|
|
const _forceRemove = function _forceRemove(node) {
|
|
arrayPush(DOMPurify.removed, { element: node });
|
|
try {
|
|
getParentNode(node).removeChild(node);
|
|
} catch (_) {
|
|
remove(node);
|
|
if (!getParentNode(node)) throw typeErrorCreate("a node selected for removal could not be detached from its tree and cannot be safely returned; refusing to sanitize in place");
|
|
}
|
|
};
|
|
/**
|
|
* _stripAttributeNode
|
|
*
|
|
* Remove a single Attr node case/namespace-exactly on an attribute-teardown
|
|
* path. Name-based removeAttribute() ASCII-lowercases its lookup key for an
|
|
* HTML element in an HTML document and so silently misses a case-preserved
|
|
* handler (e.g. `ONERROR` off an XML/XHTML import) - the same defect
|
|
* _removeAttribute() was fixed for, which a name-based call would reintroduce
|
|
* on these IN_PLACE teardown paths. Unlike _removeAttribute this does not
|
|
* record into DOMPurify.removed: the neutralize passes intentionally do not
|
|
* book-keep. A clobbered/detached node falls back to best-effort name-based
|
|
* removal.
|
|
*
|
|
* @param element the element to strip the attribute from
|
|
* @param attribute the Attr node to remove
|
|
* @param name the attribute's name, for the fallback path
|
|
*/
|
|
const _stripAttributeNode = function _stripAttributeNode(element, attribute, name) {
|
|
try {
|
|
removeAttributeNode(element, attribute);
|
|
} catch (_) {
|
|
try {
|
|
element.removeAttribute(name);
|
|
} catch (_) {}
|
|
}
|
|
};
|
|
/**
|
|
* Fail-closed teardown of an in-place root: strip every disallowed attribute
|
|
* in the subtree first, so nothing armed is left behind, then remove all
|
|
* children and the root's own attributes.
|
|
*
|
|
* @param root the in-place root to empty
|
|
*/
|
|
const _neutralizeRoot = function _neutralizeRoot(root) {
|
|
_neutralizeSubtree(root);
|
|
const childNodes = getChildNodes(root);
|
|
if (childNodes) arrayForEach(arraySlice(childNodes), (child) => {
|
|
try {
|
|
remove(child);
|
|
} catch (_) {}
|
|
});
|
|
const attributes = _readNodeType(root) === NODE_TYPE.element ? getAttributes(root) : null;
|
|
if (attributes) for (let i = attributes.length - 1; i >= 0; --i) {
|
|
const attribute = attributes[i];
|
|
const name = attribute && attribute.name;
|
|
if (typeof name === "string") _stripAttributeNode(root, attribute, name);
|
|
}
|
|
};
|
|
/**
|
|
* Removes an attribute, preferably as its exact Attr node: name-based
|
|
* removal lowercases the name in HTML documents and misses case-preserved
|
|
* names, e.g. from imported XHTML.
|
|
*
|
|
* @param name an Attribute name
|
|
* @param element a DOM node
|
|
* @param attr the exact Attr node to remove, when the caller has it
|
|
*/
|
|
const _removeAttribute = function _removeAttribute(name, element, attr) {
|
|
if (!attr) try {
|
|
attr = element.getAttributeNode(name);
|
|
} catch (_) {
|
|
attr = null;
|
|
}
|
|
arrayPush(DOMPurify.removed, {
|
|
attribute: attr || null,
|
|
from: element
|
|
});
|
|
try {
|
|
if (attr) removeAttributeNode(element, attr);
|
|
else element.removeAttribute(name);
|
|
} catch (_) {
|
|
try {
|
|
element.removeAttribute(name);
|
|
} catch (_) {}
|
|
}
|
|
if (name === "is") {
|
|
if (RETURN_DOM || RETURN_DOM_FRAGMENT) try {
|
|
_forceRemove(element);
|
|
} catch (_) {}
|
|
else try {
|
|
element.setAttribute(name, "");
|
|
} catch (_) {}
|
|
}
|
|
};
|
|
/**
|
|
* Removes every attribute the configuration does not allow. Used to
|
|
* neutralize nodes discarded from an in-place tree.
|
|
*
|
|
* @param element the element to strip
|
|
*/
|
|
const _stripDisallowedAttributes = function _stripDisallowedAttributes(element) {
|
|
const attributes = getAttributes(element);
|
|
if (!attributes) return;
|
|
for (let i = attributes.length - 1; i >= 0; --i) {
|
|
const attribute = attributes[i];
|
|
const name = attribute && attribute.name;
|
|
if (typeof name !== "string" || ALLOWED_ATTR[transformCaseFunc(name)]) continue;
|
|
_stripAttributeNode(element, attribute, name);
|
|
}
|
|
};
|
|
/**
|
|
* Iterative depth-first walk, so deep trees cannot overflow the call stack,
|
|
* reading through the cached getters, so clobbering cannot hide children.
|
|
* When `visit` returns false, the node's children are skipped. With
|
|
* `intoShadow`, open shadow roots are walked after their host's children.
|
|
*
|
|
* @param root where to start
|
|
* @param visit called for every node
|
|
* @param intoShadow whether to enter open shadow roots
|
|
*/
|
|
const _walkTree = function _walkTree(root, visit) {
|
|
let intoShadow = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : false;
|
|
const stack = [root];
|
|
while (stack.length > 0) {
|
|
const node = stack.pop();
|
|
if (visit(node) === false) continue;
|
|
if (intoShadow && _readNodeType(node) === NODE_TYPE.element) {
|
|
const shadowRoot = getShadowRoot(node);
|
|
if (_isDocumentFragment(shadowRoot)) stack.push(shadowRoot);
|
|
}
|
|
const childNodes = getChildNodes(node);
|
|
if (childNodes) for (let i = childNodes.length - 1; i >= 0; --i) stack.push(childNodes[i]);
|
|
}
|
|
};
|
|
/**
|
|
* Strips disallowed attributes from every element of a discarded subtree,
|
|
* so a handler that is already queued (a loading image) cannot fire.
|
|
*
|
|
* @param root the root of a removed subtree
|
|
*/
|
|
const _neutralizeSubtree = function _neutralizeSubtree(root) {
|
|
_walkTree(root, (node) => {
|
|
if (_readNodeType(node) === NODE_TYPE.element) _stripDisallowedAttributes(node);
|
|
});
|
|
};
|
|
/**
|
|
* All nodes under `root`, root first, including open shadow roots. Template
|
|
* content is not entered, it is inert.
|
|
*
|
|
* @param root the in-place root
|
|
* @return the nodes
|
|
*/
|
|
const _collectLiveNodes = function _collectLiveNodes(root) {
|
|
const nodes = [];
|
|
_walkTree(root, (node) => {
|
|
nodes.push(node);
|
|
}, true);
|
|
return nodes;
|
|
};
|
|
/**
|
|
* Strips disallowed attributes from every element that was under the
|
|
* in-place root when the call started and no longer is, whatever removed
|
|
* or moved it.
|
|
*
|
|
* @param root the in-place root
|
|
* @param before the root's nodes, collected before the walk
|
|
*/
|
|
const _neutralizeEscapedElements = function _neutralizeEscapedElements(root, before) {
|
|
const reachable = setCreate();
|
|
arrayForEach(_collectLiveNodes(root), (node) => {
|
|
if (_readNodeType(node) === NODE_TYPE.element) setAdd(reachable, node);
|
|
});
|
|
arrayForEach(before, (node) => {
|
|
if (_readNodeType(node) === NODE_TYPE.element && !setHas(reachable, node)) _stripDisallowedAttributes(node);
|
|
});
|
|
};
|
|
/**
|
|
* Neutralizes everything that left the in-place tree during the call: what
|
|
* DOMPurify removed, and what hooks or custom elements removed or moved out.
|
|
*
|
|
* @param root the in-place root
|
|
* @param before the root's nodes, collected before the walk
|
|
* @return true if the root itself was removed
|
|
*/
|
|
const _neutralizeDiscarded = function _neutralizeDiscarded(root, before) {
|
|
let rootRemoved = false;
|
|
arrayForEach(DOMPurify.removed, (entry) => {
|
|
if (entry.element) {
|
|
rootRemoved = rootRemoved || entry.element === root;
|
|
_neutralizeSubtree(entry.element);
|
|
}
|
|
});
|
|
_neutralizeEscapedElements(root, before);
|
|
return rootRemoved;
|
|
};
|
|
const MAX_UNVISITED_ROUNDS = 8;
|
|
/**
|
|
* The walk only moves forward, so a node moved to a position it already
|
|
* passed is never visited. Give every original node that is still in the
|
|
* tree but unvisited the normal treatment, hooks included. Nodes created
|
|
* during the call are left alone. Fails closed if the tree keeps changing.
|
|
*
|
|
* @param root the in-place root
|
|
* @param before the root's nodes, collected before the walk
|
|
*/
|
|
const _sanitizeUnvisitedNodes = function _sanitizeUnvisitedNodes(root, before) {
|
|
const visited = IN_PLACE_VISITED;
|
|
let original = null;
|
|
const getOriginal = () => {
|
|
if (!original) {
|
|
original = setCreate();
|
|
arrayForEach(before, (beforeNode) => {
|
|
setAdd(original, beforeNode);
|
|
});
|
|
}
|
|
return original;
|
|
};
|
|
for (let round = 0;; round++) {
|
|
const live = _collectLiveNodes(root);
|
|
const containers = [root];
|
|
let pending = false;
|
|
arrayForEach(live, (node) => {
|
|
if (_readNodeType(node) === NODE_TYPE.documentFragment) arrayPush(containers, node);
|
|
else if (!pending && !setHas(visited, node)) pending = setHas(getOriginal(), node);
|
|
});
|
|
if (!pending) return;
|
|
if (round >= MAX_UNVISITED_ROUNDS) throw typeErrorCreate("the tree kept changing during in-place sanitization; refusing to return it");
|
|
arrayForEach(containers, (container, index) => {
|
|
_sanitizeUnvisitedIn(container, index > 0, root, getOriginal());
|
|
});
|
|
}
|
|
};
|
|
/**
|
|
* One _sanitizeUnvisitedNodes pass over the root or an open shadow root.
|
|
*
|
|
* @param container the root or an open shadow root
|
|
* @param inShadow whether container is a shadow root
|
|
* @param root the in-place root
|
|
* @param original the root's nodes collected before the walk, as a set
|
|
*/
|
|
const _sanitizeUnvisitedIn = function _sanitizeUnvisitedIn(container, inShadow, root, original) {
|
|
const iterator = _createNodeIterator(container);
|
|
let node;
|
|
while (node = iterator.nextNode()) {
|
|
if (!setHas(original, node) || setHas(IN_PLACE_VISITED, node)) continue;
|
|
if (inShadow) _executeHooks(hooks.uponSanitizeShadowNode, node, null);
|
|
_sanitizeElements(node, root);
|
|
_sanitizeAttributes(node, root);
|
|
if (_isDocumentFragment(node.content)) _sanitizeShadowDOM2(node.content);
|
|
}
|
|
};
|
|
/**
|
|
* Records that _sanitizeElements processed `node`. No-op outside IN_PLACE.
|
|
*
|
|
* @param node the node being processed
|
|
*/
|
|
const _markVisited = function _markVisited(node) {
|
|
if (IN_PLACE_VISITED) setAdd(IN_PLACE_VISITED, node);
|
|
};
|
|
/**
|
|
* Patch-linkage attributes (declarative partial updates): `patchsrc`
|
|
* always, `for` everywhere except on <label> and <output>. Gated on
|
|
* SAFE_FOR_XML. Shared by the IN_PLACE pre-pass and _isValidAttribute.
|
|
*
|
|
* @param lcName the transformCaseFunc'd attribute name
|
|
* @param lcTag the transformCaseFunc'd tag name of the carrying element
|
|
* @return true if the attribute is patch linkage and must be dropped
|
|
*/
|
|
const _isPatchLinkageAttribute = function _isPatchLinkageAttribute(lcName, lcTag) {
|
|
if (!SAFE_FOR_XML) return false;
|
|
if (lcName === "patchsrc") return true;
|
|
return lcName === "for" && lcTag !== "label" && lcTag !== "output";
|
|
};
|
|
/**
|
|
* IN_PLACE pre-pass: sever all patch linkage before the walk, so no patch
|
|
* can fire mid-walk into a part of the tree the walker already passed.
|
|
* Removes range markers (processing instructions, comments with markup)
|
|
* and strips linkage attributes. It cannot undo a patch that already fired.
|
|
*
|
|
* TODO: if the shipped feature can patch through `for` on <label>/<output>,
|
|
* drop that pair here too (see test/declarative-patch-probe.html).
|
|
*
|
|
* @param root the in-place root to sweep
|
|
*/
|
|
const _neutralizePatchLinkage = function _neutralizePatchLinkage(root) {
|
|
if (!SAFE_FOR_XML) return;
|
|
_walkTree(root, (node) => {
|
|
const nodeType = _readNodeType(node);
|
|
if (nodeType === NODE_TYPE.processingInstruction || nodeType === NODE_TYPE.comment && regExpTest(COMMENT_MARKUP_PROBE, node.data)) {
|
|
try {
|
|
remove(node);
|
|
} catch (_) {}
|
|
return false;
|
|
}
|
|
if (nodeType === NODE_TYPE.element) {
|
|
const element = node;
|
|
const lcTag = transformCaseFunc(_readNodeName(node));
|
|
try {
|
|
if (element.hasAttribute && element.hasAttribute("patchsrc")) element.removeAttribute("patchsrc");
|
|
if (element.hasAttribute && element.hasAttribute("for") && _isPatchLinkageAttribute("for", lcTag)) element.removeAttribute("for");
|
|
} catch (_) {}
|
|
}
|
|
return true;
|
|
});
|
|
};
|
|
/**
|
|
* _initDocument
|
|
*
|
|
* @param dirty - a string of dirty markup
|
|
* @return a DOM, filled with the dirty markup
|
|
*/
|
|
const _initDocument = function _initDocument(dirty) {
|
|
let doc = null;
|
|
let leadingWhitespace = null;
|
|
if (FORCE_BODY) dirty = "<remove></remove>" + dirty;
|
|
else {
|
|
const matches = stringMatch(dirty, /^[\r\n\t ]+/);
|
|
leadingWhitespace = matches && matches[0];
|
|
}
|
|
if (PARSER_MEDIA_TYPE === "application/xhtml+xml" && NAMESPACE === HTML_NAMESPACE) dirty = "<html xmlns=\"http://www.w3.org/1999/xhtml\"><head></head><body>" + dirty + "</body></html>";
|
|
const dirtyPayload = trustedTypesPolicy ? _createTrustedHTML(dirty) : dirty;
|
|
if (NAMESPACE === HTML_NAMESPACE) try {
|
|
doc = new DOMParser().parseFromString(dirtyPayload, PARSER_MEDIA_TYPE);
|
|
} catch (_) {}
|
|
if (!doc || !doc.documentElement) {
|
|
doc = implementation.createDocument(NAMESPACE, "template", null);
|
|
try {
|
|
doc.documentElement.innerHTML = IS_EMPTY_INPUT ? emptyHTML : dirtyPayload;
|
|
} catch (_) {}
|
|
}
|
|
const body = doc.body || doc.documentElement;
|
|
if (dirty && leadingWhitespace) body.insertBefore(document.createTextNode(leadingWhitespace), body.childNodes[0] || null);
|
|
if (NAMESPACE === HTML_NAMESPACE) return getElementsByTagName.call(doc, WHOLE_DOCUMENT ? "html" : "body")[0];
|
|
return WHOLE_DOCUMENT ? doc.documentElement : body;
|
|
};
|
|
/**
|
|
* Creates a NodeIterator object that you can use to traverse filtered lists of nodes or elements in a document.
|
|
*
|
|
* @param root The root element or node to start traversing on.
|
|
* @return The created NodeIterator
|
|
*/
|
|
const _createNodeIterator = function _createNodeIterator(root) {
|
|
const doc = getOwnerDocument ? getOwnerDocument(root) : root.ownerDocument;
|
|
return createNodeIterator.call(doc || root, root, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT | NodeFilter.SHOW_PROCESSING_INSTRUCTION | NodeFilter.SHOW_CDATA_SECTION, null);
|
|
};
|
|
/**
|
|
* Replace template expression syntax (mustache, ERB, template
|
|
* literal) with a space; shared by all SAFE_FOR_TEMPLATES scrub
|
|
* sites. Order matters: mustache, then ERB, then template literal.
|
|
*
|
|
* @param value the string to scrub
|
|
* @returns the scrubbed string
|
|
*/
|
|
const _stripTemplateExpressions = function _stripTemplateExpressions(value) {
|
|
value = stringReplace(value, MUSTACHE_EXPR$1, " ");
|
|
value = stringReplace(value, ERB_EXPR$1, " ");
|
|
value = stringReplace(value, TMPLIT_EXPR$1, " ");
|
|
return value;
|
|
};
|
|
/**
|
|
* Strip template-engine expressions ({{...}}, ${...}, <%...%>) from the
|
|
* character data of an element subtree. Used as the final safety net for
|
|
* SAFE_FOR_TEMPLATES on every DOM-returning code path so that expressions
|
|
* which only form after text-node normalization (e.g. fragments split across
|
|
* stripped elements) cannot survive into a template-evaluating framework.
|
|
*
|
|
* Walks text/comment/CDATA/processing-instruction nodes and mutates `.data`
|
|
* in place rather than round-tripping through innerHTML. This preserves
|
|
* descendant node references (important for IN_PLACE callers), avoids a
|
|
* serialize/reparse cycle, and reads literal character data — which means
|
|
* `<%...%>` in text content matches the ERB regex against its real bytes
|
|
* instead of the HTML-entity-escaped form innerHTML would produce.
|
|
*
|
|
* Attribute values are not visited here; SAFE_FOR_TEMPLATES handling for
|
|
* attributes is performed during the per-node `_sanitizeAttributes` pass.
|
|
*
|
|
* @param node The root element whose character data should be scrubbed.
|
|
*/
|
|
const _scrubTemplateExpressions2 = function _scrubTemplateExpressions(node) {
|
|
var _node$querySelectorAl;
|
|
_scrubOwnCharacterData(node);
|
|
const templates = (_node$querySelectorAl = node.querySelectorAll) === null || _node$querySelectorAl === void 0 ? void 0 : _node$querySelectorAl.call(node, "template");
|
|
if (templates) arrayForEach(templates, (tmpl) => {
|
|
if (_isDocumentFragment(tmpl.content)) _scrubTemplateExpressions2(tmpl.content);
|
|
});
|
|
};
|
|
/**
|
|
* _scrubOwnCharacterData
|
|
*
|
|
* Non-recursive core of _scrubTemplateExpressions: normalize `node` and
|
|
* strip template expressions from its own character data only. Does not
|
|
* enter <template>.content or attached shadow roots (NodeIterator never
|
|
* does). Used by _sanitizeShadowDOM, where every nested template content
|
|
* and shadow root is already walked - and therefore scrubbed - by its own
|
|
* recursive _sanitizeShadowDOM call; recursing here as well would rescrub
|
|
* each nested fragment once per ancestor, which is quadratic in nesting
|
|
* depth.
|
|
*
|
|
* @param node The root whose own character data should be scrubbed.
|
|
*/
|
|
const _scrubOwnCharacterData = function _scrubOwnCharacterData(node) {
|
|
node.normalize();
|
|
const doc = getOwnerDocument ? getOwnerDocument(node) : node.ownerDocument;
|
|
const walker = createNodeIterator.call(doc || node, node, NodeFilter.SHOW_TEXT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_CDATA_SECTION | NodeFilter.SHOW_PROCESSING_INSTRUCTION, null);
|
|
let currentNode = walker.nextNode();
|
|
while (currentNode) {
|
|
currentNode.data = _stripTemplateExpressions(currentNode.data);
|
|
currentNode = walker.nextNode();
|
|
}
|
|
};
|
|
/**
|
|
* _isClobbered
|
|
*
|
|
* Detect DOM-clobbering on HTMLFormElement nodes. Form is the only HTML
|
|
* interface with [LegacyOverrideBuiltIns]; a descendant element with a
|
|
* `name` attribute matching a prototype property shadows that property
|
|
* on direct reads. We use this check at the IN_PLACE entry-point and
|
|
* during attribute sanitization to refuse clobbered forms.
|
|
*
|
|
* @param element element to check for clobbering attacks
|
|
* @return true if clobbered, false if safe
|
|
*/
|
|
const _isClobbered = function _isClobbered(element) {
|
|
const realTagName = getNodeName ? getNodeName(element) : null;
|
|
if (typeof realTagName !== "string") return false;
|
|
if (transformCaseFunc(realTagName) !== "form") return false;
|
|
return typeof element.nodeName !== "string" || typeof element.textContent !== "string" || typeof element.removeChild !== "function" || element.attributes !== getAttributes(element) || typeof element.removeAttribute !== "function" || typeof element.removeAttributeNode !== "function" || typeof element.getAttributeNode !== "function" || typeof element.setAttribute !== "function" || typeof element.namespaceURI !== "string" || typeof element.insertBefore !== "function" || typeof element.hasChildNodes !== "function" || element.nodeType !== getNodeType(element) || element.childNodes !== getChildNodes(element);
|
|
};
|
|
/**
|
|
* Checks whether the given value is a DocumentFragment from any realm.
|
|
*
|
|
* The realm-independent replacement reads `nodeType` through the cached
|
|
* Node.prototype getter and compares to the DOCUMENT_FRAGMENT_NODE
|
|
* constant (11). nodeType is a numeric value resolved from the node's
|
|
* internal slot, identical across realms for the same kind of node.
|
|
*
|
|
* @param value object to check
|
|
* @return true if value is a DocumentFragment-shaped node from any realm
|
|
*/
|
|
const _isDocumentFragment = function _isDocumentFragment(value) {
|
|
if (!getNodeType || typeof value !== "object" || value === null) return false;
|
|
try {
|
|
return getNodeType(value) === NODE_TYPE.documentFragment;
|
|
} catch (_) {
|
|
return false;
|
|
}
|
|
};
|
|
/**
|
|
* Checks whether the given object is a DOM node, including nodes that
|
|
* originate from a different window/realm (e.g. an iframe's
|
|
* contentDocument). The previous `value instanceof Node` check was
|
|
* realm-bound: nodes from a different window failed it, causing
|
|
* sanitize() to silently stringify them and reset IN_PLACE to false,
|
|
* returning the original node unsanitized. See GHSA-4w3q-35jp-p934.
|
|
*
|
|
* @param value object to check whether it's a DOM node
|
|
* @return true if value is a DOM node from any realm
|
|
*/
|
|
const _isNode = function _isNode(value) {
|
|
if (!getNodeType || typeof value !== "object" || value === null) return false;
|
|
try {
|
|
return typeof getNodeType(value) === "number";
|
|
} catch (_) {
|
|
return false;
|
|
}
|
|
};
|
|
function _executeHooks(hooks, currentNode, data) {
|
|
if (hooks.length === 0) return;
|
|
arrayForEach(hooks, (hook) => {
|
|
hook.call(DOMPurify, currentNode, data, CONFIG);
|
|
});
|
|
}
|
|
/**
|
|
* Structural-threat checks that condemn a node regardless of the
|
|
* allowlists: mXSS via namespace confusion, risky CSS construction,
|
|
* processing instructions, markup-bearing comments. Pure predicate;
|
|
* the caller removes. Check order is load-bearing.
|
|
*
|
|
* @param currentNode the node to inspect
|
|
* @param tagName the node's transformCaseFunc'd tag name
|
|
* @return true if the node must be removed
|
|
*/
|
|
const _isUnsafeNode = function _isUnsafeNode(currentNode, tagName) {
|
|
if (SAFE_FOR_XML && currentNode.hasChildNodes() && !_isNode(currentNode.firstElementChild) && regExpTest(ELEMENT_MARKUP_PROBE, currentNode.textContent) && regExpTest(ELEMENT_MARKUP_PROBE, currentNode.innerHTML)) return true;
|
|
if (SAFE_FOR_XML && LITERAL_TEXT_ELEMENTS[tagName] && _readNodeType(currentNode) === NODE_TYPE.element && _readNamespaceURI(currentNode) === HTML_NAMESPACE && (_isNode(currentNode.firstElementChild) || typeof currentNode.textContent === "string" && regExpTest(ELEMENT_MARKUP_PROBE, currentNode.textContent))) return true;
|
|
if (currentNode.nodeType === NODE_TYPE.processingInstruction) return true;
|
|
if (SAFE_FOR_XML && currentNode.nodeType === NODE_TYPE.comment && regExpTest(COMMENT_MARKUP_PROBE, currentNode.data)) return true;
|
|
return false;
|
|
};
|
|
/**
|
|
* Evaluate a CUSTOM_ELEMENT_HANDLING check (a RegExp or a predicate
|
|
* function, per the validation in _parseConfig) against a name.
|
|
* Additional arguments are forwarded to predicate functions - the
|
|
* attributeNameCheck predicate receives the tag name as its second
|
|
* argument. A null/absent check never matches.
|
|
*
|
|
* @param check the configured tagNameCheck / attributeNameCheck value
|
|
* @param name the name to test
|
|
* @param args extra arguments forwarded to a predicate function
|
|
* @return true if the check matches the name
|
|
*/
|
|
const _matchesNameCheck = function _matchesNameCheck(check, name) {
|
|
if (check instanceof RegExp) return regExpTest(check, name);
|
|
if (check instanceof Function) {
|
|
for (var _len = arguments.length, args = new Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) args[_key - 2] = arguments[_key];
|
|
return Boolean(check(name, ...args));
|
|
}
|
|
return false;
|
|
};
|
|
/**
|
|
* Handle a node whose tag is forbidden or not allowlisted: keep
|
|
* allowed custom elements (false return exits _sanitizeElements
|
|
* early - the namespace and fallback-tag removal checks are
|
|
* intentionally skipped for kept custom elements), else hoist
|
|
* content per KEEP_CONTENT and remove.
|
|
*
|
|
* A kept custom element is the ONLY case in which this function
|
|
* returns false, so the caller uses that return value to run the
|
|
* afterSanitizeElements hook on the kept element and keep the
|
|
* element-hook lifecycle consistent with normal allowlisted
|
|
* elements (GHSA-c2j3-45gr-mqc4).
|
|
*
|
|
* @param currentNode the disallowed node
|
|
* @param tagName the node's transformCaseFunc'd tag name
|
|
* @return true if the node was removed, false if kept
|
|
*/
|
|
const _sanitizeDisallowedNode = function _sanitizeDisallowedNode(currentNode, tagName, root) {
|
|
if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName) && _matchesNameCheck(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)) return false;
|
|
if (KEEP_CONTENT && !FORBID_CONTENTS[tagName] && !FORBID_CONTENTS[stringToLowerCase(tagName)]) {
|
|
const parentNode = getParentNode(currentNode);
|
|
const childNodes = getChildNodes(currentNode);
|
|
if (childNodes && parentNode) {
|
|
const childCount = childNodes.length;
|
|
for (let i = childCount - 1; i >= 0; --i) {
|
|
const hoisted = currentNode === root ? cloneNode(childNodes[i], true) : childNodes[i];
|
|
parentNode.insertBefore(hoisted, getNextSibling(currentNode));
|
|
}
|
|
}
|
|
}
|
|
_forceRemove(currentNode);
|
|
return true;
|
|
};
|
|
/**
|
|
* Fork a hook-mutable allowlist off its shared binding the first time a
|
|
* (possibly lazily-installed) uponSanitize* hook is about to see it, so the
|
|
* hook cannot widen the per-instance default or the setConfig binding by
|
|
* reference and leak past the call. Returns the set unchanged once it is
|
|
* already call-local, so repeated calls across elements are idempotent.
|
|
*
|
|
* @param hookList the uponSanitize* hook array for this event
|
|
* @param set the current ALLOWED_TAGS / ALLOWED_ATTR binding
|
|
* @param defaultSet the per-instance DEFAULT_ALLOWED_* constant
|
|
* @param setConfigSet the captured setConfig() binding, or null
|
|
* @return a call-local clone if a hook is present and set is still shared,
|
|
* else set unchanged
|
|
*/
|
|
const _forkSharedAllowlist = function _forkSharedAllowlist(hookList, set, defaultSet, setConfigSet) {
|
|
if (hookList.length === 0) return set;
|
|
return set === defaultSet || set === setConfigSet ? clone(set) : set;
|
|
};
|
|
/**
|
|
* A hook may have detached the node it was called for. If so, treat it as
|
|
* removed and stop processing it; the walk root is exempt. On the IN_PLACE
|
|
* path the detached subtree is neutralized right away.
|
|
*
|
|
* @param currentNode the node a hook may have detached
|
|
* @param root the current walk root
|
|
* @return true if the node was detached
|
|
*/
|
|
const _handleHookDetachedNode = function _handleHookDetachedNode(currentNode, root) {
|
|
if (currentNode === root || getParentNode(currentNode) !== null) return false;
|
|
if (IN_PLACE) _neutralizeSubtree(currentNode);
|
|
return true;
|
|
};
|
|
/**
|
|
* _sanitizeElements
|
|
*
|
|
* @protect nodeName
|
|
* @protect textContent
|
|
* @protect removeChild
|
|
* @param currentNode to check for permission to exist
|
|
* @return true if node was killed, false if left alive
|
|
*/
|
|
const _sanitizeElements = function _sanitizeElements(currentNode, root) {
|
|
_markVisited(currentNode);
|
|
_executeHooks(hooks.beforeSanitizeElements, currentNode, null);
|
|
if (_handleHookDetachedNode(currentNode, root)) return true;
|
|
if (_isClobbered(currentNode)) {
|
|
_forceRemove(currentNode);
|
|
return true;
|
|
}
|
|
const tagName = transformCaseFunc(_readNodeName(currentNode));
|
|
ALLOWED_TAGS = _forkSharedAllowlist(hooks.uponSanitizeElement, ALLOWED_TAGS, DEFAULT_ALLOWED_TAGS, SET_CONFIG_ALLOWED_TAGS);
|
|
_executeHooks(hooks.uponSanitizeElement, currentNode, {
|
|
tagName,
|
|
allowedTags: ALLOWED_TAGS
|
|
});
|
|
if (_handleHookDetachedNode(currentNode, root)) return true;
|
|
if (_isUnsafeNode(currentNode, tagName)) {
|
|
_forceRemove(currentNode);
|
|
return true;
|
|
}
|
|
if (FORBID_TAGS[tagName] || !(EXTRA_ELEMENT_HANDLING.tagCheck instanceof Function && EXTRA_ELEMENT_HANDLING.tagCheck(tagName)) && !ALLOWED_TAGS[tagName]) {
|
|
const removed = _sanitizeDisallowedNode(currentNode, tagName, root);
|
|
if (removed === false) {
|
|
_executeHooks(hooks.afterSanitizeElements, currentNode, null);
|
|
if (_handleHookDetachedNode(currentNode, root)) return true;
|
|
}
|
|
return removed;
|
|
}
|
|
if (_readNodeType(currentNode) === NODE_TYPE.element && !_checkValidNamespace(currentNode)) {
|
|
_forceRemove(currentNode);
|
|
return true;
|
|
}
|
|
if ((tagName === "noscript" || tagName === "noembed" || tagName === "noframes") && regExpTest(FALLBACK_TAG_CLOSE, currentNode.innerHTML)) {
|
|
_forceRemove(currentNode);
|
|
return true;
|
|
}
|
|
if (SAFE_FOR_TEMPLATES && currentNode.nodeType === NODE_TYPE.text) {
|
|
const content = _stripTemplateExpressions(currentNode.textContent);
|
|
if (currentNode.textContent !== content) {
|
|
arrayPush(DOMPurify.removed, { element: currentNode.cloneNode() });
|
|
currentNode.textContent = content;
|
|
}
|
|
}
|
|
_executeHooks(hooks.afterSanitizeElements, currentNode, null);
|
|
return _handleHookDetachedNode(currentNode, root);
|
|
};
|
|
/**
|
|
* _isValidAttribute
|
|
*
|
|
* @param lcTag Lowercase tag name of containing element.
|
|
* @param lcName Lowercase attribute name.
|
|
* @param value Attribute value.
|
|
* @return Returns true if `value` is valid, otherwise false.
|
|
*/
|
|
const _isValidAttribute = function _isValidAttribute(lcTag, lcName, value) {
|
|
if (FORBID_ATTR[lcName]) return false;
|
|
if (_isPatchLinkageAttribute(lcName, lcTag)) return false;
|
|
if (SANITIZE_DOM && (lcName === "id" || lcName === "name") && (value in document || value in formElement)) return false;
|
|
const nameIsPermitted = ALLOWED_ATTR[lcName] || EXTRA_ELEMENT_HANDLING.attributeCheck instanceof Function && EXTRA_ELEMENT_HANDLING.attributeCheck(lcName, lcTag);
|
|
if (ALLOW_DATA_ATTR && regExpTest(DATA_ATTR$1, lcName)) return true;
|
|
if (ALLOW_ARIA_ATTR && regExpTest(ARIA_ATTR$1, lcName)) return true;
|
|
if (!nameIsPermitted) return _isBasicCustomElement(lcTag) && _matchesNameCheck(CUSTOM_ELEMENT_HANDLING.tagNameCheck, lcTag) && _matchesNameCheck(CUSTOM_ELEMENT_HANDLING.attributeNameCheck, lcName, lcTag) || lcName === "is" && CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements && _matchesNameCheck(CUSTOM_ELEMENT_HANDLING.tagNameCheck, value);
|
|
if (URI_SAFE_ATTRIBUTES[lcName]) return true;
|
|
if (regExpTest(IS_ALLOWED_URI$1, stringReplace(value, ATTR_WHITESPACE$1, ""))) return true;
|
|
if ((lcName === "src" || lcName === "xlink:href" || lcName === "href") && lcTag !== "script" && stringIndexOf(value, "data:") === 0 && DATA_URI_TAGS[lcTag]) return true;
|
|
if (ALLOW_UNKNOWN_PROTOCOLS && !regExpTest(IS_SCRIPT_OR_DATA$1, stringReplace(value, ATTR_WHITESPACE$1, ""))) return true;
|
|
return !value;
|
|
};
|
|
const RESERVED_CUSTOM_ELEMENT_NAMES = addToSet({}, [
|
|
"annotation-xml",
|
|
"color-profile",
|
|
"font-face",
|
|
"font-face-format",
|
|
"font-face-name",
|
|
"font-face-src",
|
|
"font-face-uri",
|
|
"missing-glyph"
|
|
]);
|
|
/**
|
|
* _isBasicCustomElement
|
|
* checks if at least one dash is included in tagName, and it's not the first char
|
|
* for more sophisticated checking see https://github.com/sindresorhus/validate-element-name
|
|
*
|
|
* @param tagName name of the tag of the node to sanitize
|
|
* @returns Returns true if the tag name meets the basic criteria for a custom element, otherwise false.
|
|
*/
|
|
const _isBasicCustomElement = function _isBasicCustomElement(tagName) {
|
|
return !RESERVED_CUSTOM_ELEMENT_NAMES[stringToLowerCase(tagName)] && regExpTest(CUSTOM_ELEMENT$1, tagName);
|
|
};
|
|
/**
|
|
* Wrap an attribute value in the matching Trusted Types object when
|
|
* the active policy requires it. Namespaced attributes pass through
|
|
* unchanged (no TT support yet, see
|
|
* https://bugs.chromium.org/p/chromium/issues/detail?id=1305293).
|
|
*
|
|
* @param lcTag lowercase tag name of the containing element
|
|
* @param lcName lowercase attribute name
|
|
* @param namespaceURI the attribute's namespace, if any
|
|
* @param value the attribute value to wrap
|
|
* @return the value, wrapped when Trusted Types demand it
|
|
*/
|
|
const _applyTrustedTypesToAttribute = function _applyTrustedTypesToAttribute(lcTag, lcName, namespaceURI, value) {
|
|
if (trustedTypesPolicy && typeof trustedTypes === "object" && typeof trustedTypes.getAttributeType === "function" && !namespaceURI) switch (trustedTypes.getAttributeType(lcTag, lcName)) {
|
|
case "TrustedHTML": return _createTrustedHTML(value);
|
|
case "TrustedScriptURL": return _createTrustedScriptURL(value);
|
|
}
|
|
return value;
|
|
};
|
|
/**
|
|
* Write a modified attribute value back onto the element. On
|
|
* success, re-probe for clobbering introduced by the new value and
|
|
* remove the element when found; otherwise, when this writeback is the
|
|
* recreate half of the SANITIZE_NAMED_PROPS remove-and-recreate, pop the
|
|
* removal entry that path recorded so it does not show as removed. On
|
|
* failure, remove the attribute instead.
|
|
*
|
|
* Returns true only on a clean write (the value was set and the new value
|
|
* introduced no clobbering). The caller uses that, together with its own
|
|
* knowledge of whether this attribute pushed a DOMPurify.removed record, to
|
|
* decide whether to pop that record. The pop must happen ONLY for the
|
|
* named-prop remove-and-recreate; popping on any other value change (trim,
|
|
* template scrubbing, Trusted Types) would consume an unrelated _forceRemove
|
|
* subtree-cleanup record and let that detached subtree keep a live event
|
|
* handler through the IN_PLACE neutralization pass (SO-001).
|
|
*
|
|
* @param currentNode the element carrying the attribute
|
|
* @param name the attribute name as present on the element
|
|
* @param namespaceURI the attribute's namespace, if any
|
|
* @param value the new attribute value
|
|
* @return true if the value was written without introducing clobbering
|
|
*/
|
|
const _setAttributeValue = function _setAttributeValue(currentNode, name, namespaceURI, value) {
|
|
try {
|
|
if (namespaceURI) currentNode.setAttributeNS(namespaceURI, name, value);
|
|
else currentNode.setAttribute(name, value);
|
|
if (_isClobbered(currentNode)) {
|
|
_forceRemove(currentNode);
|
|
return false;
|
|
}
|
|
return true;
|
|
} catch (_) {
|
|
_removeAttribute(name, currentNode);
|
|
return false;
|
|
}
|
|
};
|
|
/**
|
|
* _isUnsafeAttributeValue
|
|
*
|
|
* Value-based attribute checks that the attribute walk applies before any
|
|
* hook can force-keep an attribute, shared with the public
|
|
* isValidAttribute() so that it never approves a value sanitize() would
|
|
* strip.
|
|
*
|
|
* @param name the attribute name as found on the node (case preserved)
|
|
* @param value the attribute value
|
|
* @returns true if the attribute must be removed because of its value
|
|
*/
|
|
const _isUnsafeAttributeValue = function _isUnsafeAttributeValue(name, value) {
|
|
if (SAFE_FOR_XML && regExpTest(/((--!?|])>)|<\/(style|script|title|xmp|textarea|noscript|iframe|noembed|noframes)/i, value)) return true;
|
|
return stringToLowerCase(name) === "attributename" && stringMatch(value, "href") !== null;
|
|
};
|
|
/**
|
|
* _sanitizeAttributes
|
|
*
|
|
* @protect attributes
|
|
* @protect nodeName
|
|
* @protect removeAttribute
|
|
* @protect setAttribute
|
|
*
|
|
* @param currentNode to sanitize
|
|
* @param root the current walk root
|
|
*/
|
|
const _sanitizeAttributes = function _sanitizeAttributes(currentNode, root) {
|
|
_executeHooks(hooks.beforeSanitizeAttributes, currentNode, null);
|
|
if (_handleHookDetachedNode(currentNode, root)) return;
|
|
const attributes = currentNode.attributes;
|
|
if (!attributes || _isClobbered(currentNode)) return;
|
|
ALLOWED_ATTR = _forkSharedAllowlist(hooks.uponSanitizeAttribute, ALLOWED_ATTR, DEFAULT_ALLOWED_ATTR, SET_CONFIG_ALLOWED_ATTR);
|
|
const hookEvent = {
|
|
attrName: "",
|
|
attrValue: "",
|
|
keepAttr: true,
|
|
allowedAttributes: ALLOWED_ATTR,
|
|
forceKeepAttr: void 0
|
|
};
|
|
let l = attributes.length;
|
|
const lcTag = transformCaseFunc(currentNode.nodeName);
|
|
while (l--) {
|
|
const attr = attributes[l];
|
|
const name = attr.name, namespaceURI = attr.namespaceURI, attrValue = attr.value;
|
|
const lcName = transformCaseFunc(name);
|
|
const initValue = attrValue;
|
|
let value = name === "value" ? initValue : stringTrim(initValue);
|
|
let recreatedNamedProp = false;
|
|
hookEvent.attrName = lcName;
|
|
hookEvent.attrValue = value;
|
|
hookEvent.keepAttr = true;
|
|
hookEvent.forceKeepAttr = void 0;
|
|
_executeHooks(hooks.uponSanitizeAttribute, currentNode, hookEvent);
|
|
value = hookEvent.attrValue;
|
|
if (SANITIZE_NAMED_PROPS && (lcName === "id" || lcName === "name") && stringIndexOf(value, SANITIZE_NAMED_PROPS_PREFIX) !== 0) {
|
|
_removeAttribute(name, currentNode, attr);
|
|
value = SANITIZE_NAMED_PROPS_PREFIX + value;
|
|
recreatedNamedProp = true;
|
|
}
|
|
if (_isUnsafeAttributeValue(name, value)) {
|
|
_removeAttribute(name, currentNode, attr);
|
|
continue;
|
|
}
|
|
if (hookEvent.forceKeepAttr) continue;
|
|
if (!hookEvent.keepAttr) {
|
|
_removeAttribute(name, currentNode, attr);
|
|
continue;
|
|
}
|
|
if (!ALLOW_SELF_CLOSE_IN_ATTR && regExpTest(SELF_CLOSING_TAG, value)) {
|
|
_removeAttribute(name, currentNode, attr);
|
|
continue;
|
|
}
|
|
if (SAFE_FOR_TEMPLATES) value = _stripTemplateExpressions(value);
|
|
if (!_isValidAttribute(lcTag, lcName, value)) {
|
|
_removeAttribute(name, currentNode, attr);
|
|
continue;
|
|
}
|
|
value = _applyTrustedTypesToAttribute(lcTag, lcName, namespaceURI, value);
|
|
if (value !== initValue) {
|
|
if (_setAttributeValue(currentNode, name, namespaceURI, value) && recreatedNamedProp) arrayPop(DOMPurify.removed);
|
|
}
|
|
}
|
|
_executeHooks(hooks.afterSanitizeAttributes, currentNode, null);
|
|
_handleHookDetachedNode(currentNode, root);
|
|
};
|
|
/**
|
|
* _sanitizeShadowDOM
|
|
*
|
|
* @param fragment to iterate over recursively
|
|
*/
|
|
const _sanitizeShadowDOM2 = function _sanitizeShadowDOM(fragment) {
|
|
let shadowNode = null;
|
|
const shadowIterator = _createNodeIterator(fragment);
|
|
_executeHooks(hooks.beforeSanitizeShadowDOM, fragment, null);
|
|
while (shadowNode = shadowIterator.nextNode()) {
|
|
_executeHooks(hooks.uponSanitizeShadowNode, shadowNode, null);
|
|
_sanitizeElements(shadowNode, fragment);
|
|
_sanitizeAttributes(shadowNode, fragment);
|
|
if (_isDocumentFragment(shadowNode.content)) _sanitizeShadowDOM2(shadowNode.content);
|
|
if (_readNodeType(shadowNode) === NODE_TYPE.element) {
|
|
const innerSr = getShadowRoot(shadowNode);
|
|
if (_isDocumentFragment(innerSr)) {
|
|
_sanitizeAttachedShadowRoots(innerSr);
|
|
_sanitizeShadowDOM2(innerSr);
|
|
}
|
|
}
|
|
}
|
|
if (SAFE_FOR_TEMPLATES) _scrubOwnCharacterData(fragment);
|
|
_executeHooks(hooks.afterSanitizeShadowDOM, fragment, null);
|
|
};
|
|
/**
|
|
* Sanitizes every attached shadow root under `root`, since the node
|
|
* iterator does not enter shadow trees. Needed for IN_PLACE input and for
|
|
* DOM input whose clonable shadow roots were copied by importNode().
|
|
*
|
|
* @param root the subtree root to walk for attached shadow roots
|
|
*/
|
|
const _sanitizeAttachedShadowRoots = function _sanitizeAttachedShadowRoots(root) {
|
|
const stack = [{
|
|
node: root,
|
|
shadow: null
|
|
}];
|
|
while (stack.length > 0) {
|
|
const item = stack.pop();
|
|
if (item.shadow) {
|
|
_sanitizeShadowDOM2(item.shadow);
|
|
continue;
|
|
}
|
|
const node = item.node;
|
|
const isElement = _readNodeType(node) === NODE_TYPE.element;
|
|
const childNodes = getChildNodes(node);
|
|
if (childNodes) for (let i = childNodes.length - 1; i >= 0; --i) stack.push({
|
|
node: childNodes[i],
|
|
shadow: null
|
|
});
|
|
if (isElement) {
|
|
const rootName = getNodeName ? getNodeName(node) : null;
|
|
if (typeof rootName === "string" && transformCaseFunc(rootName) === "template") {
|
|
const content = node.content;
|
|
if (_isDocumentFragment(content)) stack.push({
|
|
node: content,
|
|
shadow: null
|
|
});
|
|
}
|
|
}
|
|
if (isElement) {
|
|
const sr = getShadowRoot(node);
|
|
if (_isDocumentFragment(sr)) stack.push({
|
|
node: null,
|
|
shadow: sr
|
|
}, {
|
|
node: sr,
|
|
shadow: null
|
|
});
|
|
}
|
|
}
|
|
};
|
|
DOMPurify.sanitize = function(dirty) {
|
|
let cfg = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : {};
|
|
let body = null;
|
|
let importedNode = null;
|
|
let currentNode = null;
|
|
let returnNode = null;
|
|
IS_EMPTY_INPUT = !dirty;
|
|
if (IS_EMPTY_INPUT) dirty = "<!-->";
|
|
if (typeof dirty !== "string" && !_isNode(dirty)) {
|
|
dirty = stringifyValue(dirty);
|
|
if (typeof dirty !== "string") throw typeErrorCreate("dirty is not a string, aborting");
|
|
}
|
|
if (!DOMPurify.isSupported) return dirty;
|
|
if (SET_CONFIG) {
|
|
ALLOWED_TAGS = SET_CONFIG_ALLOWED_TAGS;
|
|
ALLOWED_ATTR = SET_CONFIG_ALLOWED_ATTR;
|
|
} else _parseConfig(cfg);
|
|
if (hooks.uponSanitizeElement.length > 0 || hooks.uponSanitizeAttribute.length > 0) ALLOWED_TAGS = clone(ALLOWED_TAGS);
|
|
if (hooks.uponSanitizeAttribute.length > 0) ALLOWED_ATTR = clone(ALLOWED_ATTR);
|
|
DOMPurify.removed = [];
|
|
const inPlace = IN_PLACE && typeof dirty !== "string" && _isNode(dirty);
|
|
let inPlaceNodes = [];
|
|
const outerVisited = IN_PLACE_VISITED;
|
|
if (inPlace) {
|
|
_neutralizePatchLinkage(dirty);
|
|
const nn = _readNodeName(dirty);
|
|
if (typeof nn === "string") {
|
|
const tagName = transformCaseFunc(nn);
|
|
if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) {
|
|
_neutralizeRoot(dirty);
|
|
throw typeErrorCreate("root node is forbidden and cannot be sanitized in-place");
|
|
}
|
|
}
|
|
if (_isClobbered(dirty)) {
|
|
_neutralizeRoot(dirty);
|
|
throw typeErrorCreate("root node is clobbered and cannot be sanitized in-place");
|
|
}
|
|
inPlaceNodes = _collectLiveNodes(dirty);
|
|
IN_PLACE_VISITED = setCreate();
|
|
} else if (_isNode(dirty)) {
|
|
body = _initDocument("<!---->");
|
|
importedNode = body.ownerDocument.importNode(dirty, true);
|
|
if (importedNode.nodeType === NODE_TYPE.element && importedNode.nodeName === "BODY") body = importedNode;
|
|
else if (importedNode.nodeName === "HTML") body = importedNode;
|
|
else body.appendChild(importedNode);
|
|
_sanitizeAttachedShadowRoots(body);
|
|
} else {
|
|
if (!RETURN_DOM && !SAFE_FOR_TEMPLATES && !WHOLE_DOCUMENT && dirty.indexOf("<") === -1) return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? _createTrustedHTML(dirty) : dirty;
|
|
body = _initDocument(dirty);
|
|
if (!body) return RETURN_DOM ? null : RETURN_TRUSTED_TYPE ? emptyHTML : "";
|
|
}
|
|
if (body && FORCE_BODY) _forceRemove(body.firstChild);
|
|
const walkRoot = inPlace ? dirty : body;
|
|
try {
|
|
if (inPlace) _sanitizeAttachedShadowRoots(dirty);
|
|
const nodeIterator = _createNodeIterator(walkRoot);
|
|
while (currentNode = nodeIterator.nextNode()) {
|
|
_sanitizeElements(currentNode, walkRoot);
|
|
_sanitizeAttributes(currentNode, walkRoot);
|
|
if (_isDocumentFragment(currentNode.content)) _sanitizeShadowDOM2(currentNode.content);
|
|
}
|
|
if (inPlace) _sanitizeUnvisitedNodes(dirty, inPlaceNodes);
|
|
} catch (error) {
|
|
if (inPlace) {
|
|
_neutralizeRoot(dirty);
|
|
_neutralizeDiscarded(dirty, inPlaceNodes);
|
|
}
|
|
throw error;
|
|
} finally {
|
|
IN_PLACE_VISITED = outerVisited;
|
|
}
|
|
if (inPlace) {
|
|
if (_neutralizeDiscarded(dirty, inPlaceNodes)) throw typeErrorCreate("a node selected for removal could not be safely returned; refusing to sanitize in place");
|
|
if (SAFE_FOR_TEMPLATES) _scrubTemplateExpressions2(dirty);
|
|
return dirty;
|
|
}
|
|
if (RETURN_DOM) {
|
|
if (SAFE_FOR_TEMPLATES) _scrubTemplateExpressions2(body);
|
|
if (RETURN_DOM_FRAGMENT) {
|
|
returnNode = createDocumentFragment.call(body.ownerDocument);
|
|
while (body.firstChild) returnNode.appendChild(body.firstChild);
|
|
} else returnNode = body;
|
|
if (ALLOWED_ATTR.shadowroot || ALLOWED_ATTR.shadowrootmode) returnNode = importNode.call(originalDocument, returnNode, true);
|
|
return returnNode;
|
|
}
|
|
let serializedHTML = WHOLE_DOCUMENT ? body.outerHTML : body.innerHTML;
|
|
if (WHOLE_DOCUMENT && ALLOWED_TAGS["!doctype"] && body.ownerDocument && body.ownerDocument.doctype && body.ownerDocument.doctype.name && regExpTest(DOCTYPE_NAME, body.ownerDocument.doctype.name)) serializedHTML = "<!DOCTYPE " + body.ownerDocument.doctype.name + ">\n" + serializedHTML;
|
|
if (SAFE_FOR_TEMPLATES) serializedHTML = _stripTemplateExpressions(serializedHTML);
|
|
return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? _createTrustedHTML(serializedHTML) : serializedHTML;
|
|
};
|
|
DOMPurify.setConfig = function() {
|
|
let cfg = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : {};
|
|
_parseConfig(cfg);
|
|
SET_CONFIG = true;
|
|
SET_CONFIG_ALLOWED_TAGS = ALLOWED_TAGS;
|
|
SET_CONFIG_ALLOWED_ATTR = ALLOWED_ATTR;
|
|
};
|
|
DOMPurify.clearConfig = function() {
|
|
CONFIG = null;
|
|
SET_CONFIG = false;
|
|
SET_CONFIG_ALLOWED_TAGS = null;
|
|
SET_CONFIG_ALLOWED_ATTR = null;
|
|
trustedTypesPolicy = defaultTrustedTypesPolicy;
|
|
emptyHTML = "";
|
|
};
|
|
DOMPurify.isValidAttribute = function(tag, attr, value) {
|
|
if (!CONFIG) _parseConfig({});
|
|
const lcTag = transformCaseFunc(tag);
|
|
const lcName = transformCaseFunc(attr);
|
|
if (typeof value === "string" && (_isUnsafeAttributeValue(attr, value) || !ALLOW_SELF_CLOSE_IN_ATTR && regExpTest(SELF_CLOSING_TAG, value))) return false;
|
|
return _isValidAttribute(lcTag, lcName, value);
|
|
};
|
|
DOMPurify.addHook = function(entryPoint, hookFunction) {
|
|
if (typeof hookFunction !== "function") return;
|
|
if (!objectHasOwnProperty(hooks, entryPoint)) return;
|
|
arrayPush(hooks[entryPoint], hookFunction);
|
|
};
|
|
DOMPurify.removeHook = function(entryPoint, hookFunction) {
|
|
if (!objectHasOwnProperty(hooks, entryPoint)) return;
|
|
if (hookFunction !== void 0) {
|
|
const index = arrayLastIndexOf(hooks[entryPoint], hookFunction);
|
|
return index === -1 ? void 0 : arraySplice(hooks[entryPoint], index, 1)[0];
|
|
}
|
|
return arrayPop(hooks[entryPoint]);
|
|
};
|
|
DOMPurify.removeHooks = function(entryPoint) {
|
|
if (!objectHasOwnProperty(hooks, entryPoint)) return;
|
|
hooks[entryPoint] = [];
|
|
};
|
|
DOMPurify.removeAllHooks = function() {
|
|
hooks = _createHooksMap();
|
|
};
|
|
return DOMPurify;
|
|
}
|
|
return createDOMPurify();
|
|
});
|
|
|
|
//# sourceMappingURL=purify.js.map
|