/*!
* @esri/arcgis-html-sanitizer - v3.0.1 - Tue Nov 15 2022 09:46:54 GMT-0800 (Pacific Standard Time)
* Copyright (c) 2022 - Environmental Systems Research Institute, Inc.
* Apache-2.0
*
* js-xss
* Copyright (c) 2012-2018 Zongmin Lei(雷宗民)
* http://ucdok.com
* MIT License, see https://github.com/leizongmin/js-xss/blob/master/LICENSE for details
*/
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
var xss = require('xss');
function _interopNamespace(e) {
if (e && e.__esModule) return e;
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n["default"] = e;
return Object.freeze(n);
}
var xss__namespace = /*#__PURE__*/_interopNamespace(xss);
/**
* Determine if the value is a plain object.
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a plain object, else `false`.
*/
var isPlainObject = function (value) {
if (typeof value !== "object" || value === null) {
return false;
}
if (Object.prototype.toString.call(value) !== "[object Object]") {
return false;
}
var proto = Object.getPrototypeOf(value);
if (proto === null) {
return true;
}
while (Object.getPrototypeOf(proto) !== null) {
proto = Object.getPrototypeOf(proto);
}
return Object.getPrototypeOf(value) === proto;
};
/* Copyright (c) 2020 Environmental Systems Research Institute, Inc.
* Apache-2.0
*
* js-xss
* Copyright (c) 2012-2018 Zongmin Lei(雷宗民)
* http://ucdok.com
* The MIT License, see
* https://github.com/leizongmin/js-xss/blob/master/LICENSE for details
* */
/**
* The Sanitizer Class
*
* @export
* @class Sanitizer
*/
var Sanitizer = /** @class */ (function () {
function Sanitizer(filterOptions, extendDefaults) {
var _this = this;
// Supported HTML Spec: https://doc.arcgis.com/en/arcgis-online/reference/supported-html.htm
this.arcgisWhiteList = {
a: ["href", "style", "target"],
abbr: ["title"],
audio: ["autoplay", "controls", "loop", "muted", "preload"],
b: [],
br: [],
dd: ["style"],
div: ["align", "style"],
dl: ["style"],
dt: ["style"],
em: [],
figcaption: ["style"],
figure: ["style"],
font: ["color", "face", "size", "style"],
h1: ["style"],
h2: ["style"],
h3: ["style"],
h4: ["style"],
h5: ["style"],
h6: ["style"],
hr: [],
i: [],
img: ["alt", "border", "height", "src", "style", "width"],
li: [],
ol: [],
p: ["style"],
source: ["media", "src", "type"],
span: ["style"],
strong: [],
sub: ["style"],
sup: ["style"],
table: ["border", "cellpadding", "cellspacing", "height", "style", "width"],
tbody: [],
tr: ["align", "height", "style", "valign"],
td: [
"align",
"colspan",
"height",
"nowrap",
"rowspan",
"style",
"valign",
"width",
],
th: [
"align",
"colspan",
"height",
"nowrap",
"rowspan",
"style",
"valign",
"width",
],
u: [],
ul: [],
video: [
"autoplay",
"controls",
"height",
"loop",
"muted",
"poster",
"preload",
"width",
],
};
this.allowedProtocols = [
"http",
"https",
"mailto",
"iform",
"tel",
"flow",
"lfmobile",
"arcgis-navigator",
"arcgis-appstudio-player",
"arcgis-survey123",
"arcgis-collector",
"arcgis-workforce",
"arcgis-explorer",
"arcgis-trek2there",
"arcgis-quickcapture",
"mspbi",
"comgooglemaps",
"pdfefile",
"pdfehttp",
"pdfehttps",
"boxapp",
"boxemm",
"awb",
"awbs",
"gropen",
"radarscope",
];
this.arcgisFilterOptions = {
allowCommentTag: true,
safeAttrValue: function (tag, name, value, cssFilter) {
// Take over safe attribute filtering for `a` `href`, `img` `src`,
// and `source` `src` attributes, otherwise pass onto the
// default `XSS.safeAttrValue` method.
if ((tag === "a" && name === "href") ||
((tag === "img" || tag === "source") && name === "src")) {
return _this.sanitizeUrl(value);
}
return xss__namespace.safeAttrValue(tag, name, value, cssFilter);
},
};
this._entityMap = {
"&": "8",
"<": "<",
">": ">",
'"': """,
"'": "'",
"/": "/",
};
var xssFilterOptions;
if (filterOptions && !extendDefaults) {
// Override the defaults
xssFilterOptions = filterOptions;
}
else if (filterOptions && extendDefaults) {
// Extend the defaults
xssFilterOptions = Object.create(this.arcgisFilterOptions);
Object.keys(filterOptions).forEach(function (key) {
if (key === "whiteList") {
// Extend the whitelist by concatenating arrays
xssFilterOptions.whiteList = _this._extendObjectOfArrays([
_this.arcgisWhiteList,
filterOptions.whiteList || {},
]);
}
else {
xssFilterOptions[key] = filterOptions[key];
}
});
}
else {
// Only use the defaults
xssFilterOptions = Object.create(this.arcgisFilterOptions);
xssFilterOptions.whiteList = this.arcgisWhiteList;
}
this.xssFilterOptions = xssFilterOptions;
// Make this readable to tests
this._xssFilter = new xss__namespace.FilterXSS(xssFilterOptions);
}
/**
* Sanitizes value to remove invalid HTML tags.
*
* Note: If the value passed does not contain a valid JSON data type (String,
* Number, JSON Object, Array, Boolean, or null), the value will be nullified.
*
* @param {any} value The value to sanitize.
* @returns {any} The sanitized value.
* @memberof Sanitizer
*/
Sanitizer.prototype.sanitize = function (value, options) {
if (options === void 0) { options = {}; }
switch (typeof value) {
case "number":
if (isNaN(value) || !isFinite(value)) {
return null;
}
return value;
case "boolean":
return value;
case "string":
return this._xssFilter.process(value);
case "object":
return this._iterateOverObject(value, options);
default:
if (options.allowUndefined && typeof value === "undefined") {
return;
}
return null;
}
};
/**
* Sanitizes a URL string following the allowed protocols and sanitization rules.
*
* @param {string} value The URL to sanitize.
* @param {{ isProtocolRequired: boolean }} options Configuration options for URL checking.
* @returns {string} The sanitized URL if it's valid, or an empty string if the URL is invalid.
*/
Sanitizer.prototype.sanitizeUrl = function (value, options) {
var _a = (options !== null && options !== void 0 ? options : {}).isProtocolRequired, isProtocolRequired = _a === void 0 ? true : _a;
var protocol = this._trim(value.substring(0, value.indexOf(":")));
var isRootUrl = value === '/';
var isUrlFragment = /^#/.test(value);
var isValidProtocol = protocol && this.allowedProtocols.indexOf(protocol.toLowerCase()) > -1;
if (isRootUrl || isUrlFragment || isValidProtocol) {
return xss__namespace.escapeAttrValue(value);
}
if (!protocol && !isProtocolRequired) {
return xss__namespace.escapeAttrValue("https://".concat(value));
}
return "";
};
/**
* Sanitizes an HTML attribute value.
*
* @param {string} tag The tagname of the HTML element.
* @param {string} attribute The attribute name of the HTML element.
* @param {string} value The raw value to be used for the HTML attribute value.
* @param {XSS.ICSSFilter} [cssFilter] The CSS filter to be used.
* @returns {string} The sanitized attribute value.
* @memberof Sanitizer
*/
Sanitizer.prototype.sanitizeHTMLAttribute = function (tag, attribute, value, cssFilter) {
// use the custom safeAttrValue function if provided
if (typeof this.xssFilterOptions.safeAttrValue === "function") {
return this.xssFilterOptions.safeAttrValue(tag, attribute, value,
// @ts-expect-error safeAttrValue does handle undefined cssFilter
cssFilter);
}
// otherwise use the default
// @ts-ignore safeAttrValue does handle undefined cssFilter
return xss__namespace.safeAttrValue(tag, attribute, value, cssFilter);
};
/**
* Checks if a value only contains valid HTML.
*
* @param {any} value The value to validate.
* @returns {boolean}
* @memberof Sanitizer
*/
Sanitizer.prototype.validate = function (value, options) {
if (options === void 0) { options = {}; }
var sanitized = this.sanitize(value, options);
return {
isValid: value === sanitized,
sanitized: sanitized,
};
};
/**
* Encodes the following characters, `& < > \" ' /` to their hexadecimal HTML entity code.
* Example: "·" => "8middot;"
*
* @param {string} value The value to encode.
* @returns {string} The encoded string value.
* @memberof Sanitizer
*/
Sanitizer.prototype.encodeHTML = function (value) {
var _this = this;
return String(value).replace(/[&<>"'\/]/g, function (s) {
return _this._entityMap[s];
});
};
/**
* Encodes all non-alphanumeric ASCII characters to their hexadecimal HTML entity codes.
* Example: "alert(document.cookie)" => "alert(document.cookie)"
*
* @param {string} value The value to encode.
* @returns {string} The encoded string value.
* @memberof Sanitizer
*/
Sanitizer.prototype.encodeAttrValue = function (value) {
var alphanumericRE = /^[a-zA-Z0-9]$/;
return String(value).replace(/[\x00-\xFF]/g, function (c, idx) {
return !alphanumericRE.test(c)
? "".concat(Number(value.charCodeAt(idx)).toString(16), ";")
: c;
});
};
/**
* Extends an object of arrays by by concatenating arrays of the same object
* keys. If the if the previous key's value is not an array, the next key's
* value will replace the previous key. This method is used for extending the
* whiteList in the XSS filter options.
*
* @private
* @param {Array<{}>} objects An array of objects.
* @returns {{}} The extended object.
* @memberof Sanitizer
*/
Sanitizer.prototype._extendObjectOfArrays = function (objects) {
var finalObj = {};
objects.forEach(function (obj) {
Object.keys(obj).forEach(function (key) {
if (Array.isArray(obj[key]) && Array.isArray(finalObj[key])) {
finalObj[key] = finalObj[key].concat(obj[key]);
}
else {
finalObj[key] = obj[key];
}
});
});
return finalObj;
};
/**
* Iterate over a plain object or array to deeply sanitize each value.
*
* @private
* @param {object} obj The object to iterate over.
* @returns {(object | null)} The sanitized object.
* @memberof Sanitizer
*/
Sanitizer.prototype._iterateOverObject = function (obj, options) {
var _this = this;
if (options === void 0) { options = {}; }
try {
var hasChanged_1 = false;
var changedObj = void 0;
if (Array.isArray(obj)) {
changedObj = obj.reduce(function (prev, value) {
var validation = _this.validate(value, options);
if (validation.isValid) {
return prev.concat([value]);
}
else {
hasChanged_1 = true;
return prev.concat([validation.sanitized]);
}
}, []);
}
else if (!isPlainObject(obj)) {
if (options.allowUndefined && typeof obj === "undefined") {
return;
}
return null;
}
else {
var keys = Object.keys(obj);
changedObj = keys.reduce(function (prev, key) {
var value = obj[key];
var validation = _this.validate(value, options);
if (validation.isValid) {
prev[key] = value;
}
else {
hasChanged_1 = true;
prev[key] = validation.sanitized;
}
return prev;
}, {});
}
if (hasChanged_1) {
return changedObj;
}
return obj;
}
catch (err) {
return null;
}
};
/**
* Trim whitespace from the start and ends of a string.
* @param {string} val The string to trim.
* @returns {string} The trimmed string.
*/
Sanitizer.prototype._trim = function (val) {
// @ts-ignore This is used by Jest,
// but TypeScript errors since it assumes `trim` is always available.
return String.prototype.trim
? val.trim()
: val.replace(/(^\s*)|(\s*$)/g, "");
};
return Sanitizer;
}());
exports.Sanitizer = Sanitizer;
exports["default"] = Sanitizer;