| /* | |
| Copyright (c) 2014, Yahoo! Inc. All rights reserved. | |
| Copyrights licensed under the New BSD License. | |
| See the accompanying LICENSE file for terms. | |
| */ | |
| 'use strict'; | |
| // Generate an internal UID to make the regexp pattern harder to guess. | |
| var UID_LENGTH = 16; | |
| var UID = generateUID(); | |
| var PLACE_HOLDER_REGEXP = new RegExp('(\\\\)?"@__(F|R|D|M|S|A|U|I|B|L)-' + UID + '-(\\d+)__@"', 'g'); | |
| var IS_NATIVE_CODE_REGEXP = /\{\s*\[native code\]\s*\}/g; | |
| var IS_PURE_FUNCTION = /function.*?\(/; | |
| var IS_ARROW_FUNCTION = /.*?=>.*?/; | |
| var UNSAFE_CHARS_REGEXP = /[<>\/\u2028\u2029]/g; | |
| // Matches a script end tag (case-insensitive) for XSS protection: either a | |
| // full `</script...>` tag, or a bare `</script` followed by one of the | |
| // characters (TAB, LF, FF, CR, SPACE, `/`, `>`) that the HTML tokenizer | |
| // treats as ending the tag name (see the WHATWG "script data end tag name | |
| // state"). The bare-prefix form matters because the matching `>` could be | |
| // supplied by a different serialized value later in the output, so escaping | |
| // stops there without waiting for a closing `>`. A trailing backslash is not | |
| // a delimiter here (that's a JS-level concern, not an HTML one), so this | |
| // doesn't affect tagged template literals like `String.raw`. | |
| var SCRIPT_CLOSE_REGEXP = /<\/script[^>]*>|<\/script(?=[\t\n\f\r \/>])/gi; | |
| var RESERVED_SYMBOLS = ['*', 'async']; | |
| // Mapping of unsafe HTML and invalid JavaScript line terminator chars to their | |
| // Unicode char counterparts which are safe to use in JavaScript strings. | |
| var ESCAPED_CHARS = { | |
| '<' : '\\u003C', | |
| '>' : '\\u003E', | |
| '/' : '\\u002F', | |
| '\u2028': '\\u2028', | |
| '\u2029': '\\u2029' | |
| }; | |
| function escapeUnsafeChars(unsafeChar) { | |
| return ESCAPED_CHARS[unsafeChar]; | |
| } | |
| // Roughly matches string literals, template literals, regex literals, and | |
| // comments, so `escapeFunctionBody` can treat their contents differently | |
| // from plain code. This is a heuristic, not a full parser, with two known | |
| // limitations: | |
| // - A template literal is treated as one opaque span, including any | |
| // `${...}` inside it. A `</script`-like sequence inside such a | |
| // substitution (real code) can be misclassified as string content. | |
| // - The regex-literal alternative can't tell a regex from division (e.g. | |
| // `a / b / c`); a division expression that matches it is simply treated | |
| // as opaque, which only risks an unnecessary (but valid) unicode-escape. | |
| // It exists mainly so quotes and slashes inside a real regex (including | |
| // inside a `[...]` character class) aren't mistaken for string/regex | |
| // delimiters, which would misalign whatever follows. | |
| var STRING_OR_COMMENT_REGEXP = /\/\*[\s\S]*?\*\/|\/\/[^\n]*|'(?:\\.|[^'\\])*'|"(?:\\.|[^"\\])*"|`(?:\\.|[^`\\])*`|\/(?:\\.|\[(?:\\.|[^\]\\\n])*\]|[^\/\\\n])+\//g; | |
| // Escape function body for XSS protection while preserving arrow function | |
| // syntax (=>), comparison operators, and regex literals: only script end | |
| // tags and line terminators are escaped. | |
| function escapeFunctionBody(str) { | |
| // Record the [start, end) span of every string/template/regex literal | |
| // and comment, so a script-close match inside one can be escaped | |
| // differently from a match in plain code (see below). | |
| var stringAndCommentSpans = []; | |
| var match; | |
| STRING_OR_COMMENT_REGEXP.lastIndex = 0; | |
| while ((match = STRING_OR_COMMENT_REGEXP.exec(str))) { | |
| stringAndCommentSpans.push([match.index, match.index + match[0].length]); | |
| } | |
| // Matches and spans are both in increasing offset order, so a single | |
| // forward-moving cursor classifies every match in O(n) total instead of | |
| // rescanning all spans for each match. | |
| var spanCursor = 0; | |
| str = str.replace(SCRIPT_CLOSE_REGEXP, function(scriptCloseMatch, offset) { | |
| while (spanCursor < stringAndCommentSpans.length && stringAndCommentSpans[spanCursor][1] <= offset) { | |
| spanCursor++; | |
| } | |
| var span = stringAndCommentSpans[spanCursor]; | |
| var inStringOrComment = !!span && offset >= span[0] && offset < span[1]; | |
| if (!inStringOrComment) { | |
| // In plain code, `<` and `/` may be real tokens (comparison, | |
| // regex delimiter, division, ...), so they can't be rewritten | |
| // as unicode escapes without breaking syntax. A space is a | |
| // no-op here but still breaks up the `</script` sequence. | |
| return '< ' + scriptCloseMatch.slice(1); | |
| } | |
| // Inside a string/template/regex/comment, characters must be | |
| // preserved exactly, so unicode-escape instead. | |
| return scriptCloseMatch.replace(/</g, '\\u003C').replace(/\//g, '\\u002F').replace(/>/g, '\\u003E'); | |
| }); | |
| str = str.replace(/\u2028/g, '\\u2028'); | |
| str = str.replace(/\u2029/g, '\\u2029'); | |
| return str; | |
| } | |
| function generateUID() { | |
| var bytes = crypto.getRandomValues(new Uint8Array(UID_LENGTH)); | |
| var result = ''; | |
| for(var i=0; i<UID_LENGTH; ++i) { | |
| result += bytes[i].toString(16); | |
| } | |
| return result; | |
| } | |
| function deleteFunctions(obj){ | |
| var functionKeys = []; | |
| for (var key in obj) { | |
| if (typeof obj[key] === "function") { | |
| functionKeys.push(key); | |
| } | |
| } | |
| for (var i = 0; i < functionKeys.length; i++) { | |
| delete obj[functionKeys[i]]; | |
| } | |
| } | |
| module.exports = function serialize(obj, options) { | |
| options || (options = {}); | |
| // Backwards-compatibility for `space` as the second argument. | |
| if (typeof options === 'number' || typeof options === 'string') { | |
| options = {space: options}; | |
| } | |
| var functions = []; | |
| var regexps = []; | |
| var dates = []; | |
| var maps = []; | |
| var sets = []; | |
| var arrays = []; | |
| var undefs = []; | |
| var infinities= []; | |
| var bigInts = []; | |
| var urls = []; | |
| // Returns placeholders for functions and regexps (identified by index) | |
| // which are later replaced by their string representation. | |
| function replacer(key, value) { | |
| // For nested function | |
| if(options.ignoreFunction){ | |
| deleteFunctions(value); | |
| } | |
| if (!value && value !== undefined && value !== BigInt(0)) { | |
| return value; | |
| } | |
| // If the value is an object w/ a toJSON method, toJSON is called before | |
| // the replacer runs, so we use this[key] to get the non-toJSONed value. | |
| var origValue = this[key]; | |
| var type = typeof origValue; | |
| if (type === 'object') { | |
| if(origValue instanceof RegExp) { | |
| return '@__R-' + UID + '-' + (regexps.push(origValue) - 1) + '__@'; | |
| } | |
| if(origValue instanceof Date) { | |
| return '@__D-' + UID + '-' + (dates.push(origValue) - 1) + '__@'; | |
| } | |
| if(origValue instanceof Map) { | |
| return '@__M-' + UID + '-' + (maps.push(origValue) - 1) + '__@'; | |
| } | |
| if(origValue instanceof Set) { | |
| return '@__S-' + UID + '-' + (sets.push(origValue) - 1) + '__@'; | |
| } | |
| if(Array.isArray(origValue)) { | |
| var isSparse = Object.keys(origValue).length !== origValue.length; | |
| if (isSparse) { | |
| return '@__A-' + UID + '-' + (arrays.push(origValue) - 1) + '__@'; | |
| } | |
| } | |
| if(origValue instanceof URL) { | |
| return '@__L-' + UID + '-' + (urls.push(origValue) - 1) + '__@'; | |
| } | |
| } | |
| if (type === 'function') { | |
| return '@__F-' + UID + '-' + (functions.push(origValue) - 1) + '__@'; | |
| } | |
| if (type === 'undefined') { | |
| return '@__U-' + UID + '-' + (undefs.push(origValue) - 1) + '__@'; | |
| } | |
| if (type === 'number' && !isNaN(origValue) && !isFinite(origValue)) { | |
| return '@__I-' + UID + '-' + (infinities.push(origValue) - 1) + '__@'; | |
| } | |
| if (type === 'bigint') { | |
| return '@__B-' + UID + '-' + (bigInts.push(origValue) - 1) + '__@'; | |
| } | |
| return value; | |
| } | |
| function serializeFunc(fn, options) { | |
| var serializedFn = fn.toString(); | |
| if (IS_NATIVE_CODE_REGEXP.test(serializedFn)) { | |
| throw new TypeError('Serializing native function: ' + fn.name); | |
| } | |
| // Escape unsafe HTML characters in function body for XSS protection | |
| if (options && options.unsafe !== true) { | |
| serializedFn = escapeFunctionBody(serializedFn); | |
| } | |
| // pure functions, example: {key: function() {}} | |
| if(IS_PURE_FUNCTION.test(serializedFn)) { | |
| return serializedFn; | |
| } | |
| // arrow functions, example: arg1 => arg1+5 | |
| if(IS_ARROW_FUNCTION.test(serializedFn)) { | |
| return serializedFn; | |
| } | |
| var argsStartsAt = serializedFn.indexOf('('); | |
| var def = serializedFn.substr(0, argsStartsAt) | |
| .trim() | |
| .split(' ') | |
| .filter(function(val) { return val.length > 0 }); | |
| var nonReservedSymbols = def.filter(function(val) { | |
| return RESERVED_SYMBOLS.indexOf(val) === -1 | |
| }); | |
| // enhanced literal objects, example: {key() {}} | |
| if(nonReservedSymbols.length > 0) { | |
| return (def.indexOf('async') > -1 ? 'async ' : '') + 'function' | |
| + (def.join('').indexOf('*') > -1 ? '*' : '') | |
| + serializedFn.substr(argsStartsAt); | |
| } | |
| // arrow functions | |
| return serializedFn; | |
| } | |
| // Check if the parameter is function | |
| if (options.ignoreFunction && typeof obj === "function") { | |
| obj = undefined; | |
| } | |
| // Protects against `JSON.stringify()` returning `undefined`, by serializing | |
| // to the literal string: "undefined". | |
| if (obj === undefined) { | |
| return String(obj); | |
| } | |
| var str; | |
| // Creates a JSON string representation of the value. | |
| // NOTE: Node 0.12 goes into slow mode with extra JSON.stringify() args. | |
| if (options.isJSON && !options.space) { | |
| str = JSON.stringify(obj); | |
| } else { | |
| str = JSON.stringify(obj, options.isJSON ? null : replacer, options.space); | |
| } | |
| // Protects against `JSON.stringify()` returning `undefined`, by serializing | |
| // to the literal string: "undefined". | |
| if (typeof str !== 'string') { | |
| return String(str); | |
| } | |
| // Replace unsafe HTML and invalid JavaScript line terminator chars with | |
| // their safe Unicode char counterpart. This _must_ happen before the | |
| // regexps and functions are serialized and added back to the string. | |
| if (options.unsafe !== true) { | |
| str = str.replace(UNSAFE_CHARS_REGEXP, escapeUnsafeChars); | |
| } | |
| if (functions.length === 0 && regexps.length === 0 && dates.length === 0 && maps.length === 0 && sets.length === 0 && arrays.length === 0 && undefs.length === 0 && infinities.length === 0 && bigInts.length === 0 && urls.length === 0) { | |
| return str; | |
| } | |
| // Replaces all occurrences of function, regexp, date, map and set placeholders in the | |
| // JSON string with their string representations. If the original value can | |
| // not be found, then `undefined` is used. | |
| return str.replace(PLACE_HOLDER_REGEXP, function (match, backSlash, type, valueIndex) { | |
| // The placeholder may not be preceded by a backslash. This is to prevent | |
| // replacing things like `"a\"@__R-<UID>-0__@"` and thus outputting | |
| // invalid JS. | |
| if (backSlash) { | |
| return match; | |
| } | |
| if (type === 'D') { | |
| // Validate ISO string format to prevent code injection via spoofed toISOString() | |
| var isoStr = String(dates[valueIndex].toISOString()); | |
| if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z$/.test(isoStr)) { | |
| throw new TypeError('Invalid Date ISO string'); | |
| } | |
| return "new Date(\"" + isoStr + "\")"; | |
| } | |
| if (type === 'R') { | |
| // Sanitize flags to prevent code injection (only allow valid RegExp flag characters) | |
| var flags = String(regexps[valueIndex].flags).replace(/[^gimsuydv]/g, ''); | |
| var regexpSource = regexps[valueIndex].source; | |
| if (typeof regexpSource !== 'string') { | |
| throw new TypeError('RegExp.source must be a string'); | |
| } | |
| return "new RegExp(" + serialize(regexpSource) + ", \"" + flags + "\")"; | |
| } | |
| if (type === 'M') { | |
| return "new Map(" + serialize(Array.from(maps[valueIndex].entries()), options) + ")"; | |
| } | |
| if (type === 'S') { | |
| return "new Set(" + serialize(Array.from(sets[valueIndex].values()), options) + ")"; | |
| } | |
| if (type === 'A') { | |
| return "Array.prototype.slice.call(" + serialize(Object.assign({ length: arrays[valueIndex].length }, arrays[valueIndex]), options) + ")"; | |
| } | |
| if (type === 'U') { | |
| return 'undefined' | |
| } | |
| if (type === 'I') { | |
| return infinities[valueIndex]; | |
| } | |
| if (type === 'B') { | |
| return "BigInt(\"" + bigInts[valueIndex] + "\")"; | |
| } | |
| if (type === 'L') { | |
| var urlStr = urls[valueIndex].toString(); | |
| if (typeof urlStr !== 'string') { | |
| throw new TypeError('URL.toString() must return a string'); | |
| } | |
| return "new URL(" + serialize(urlStr, options) + ")"; | |
| } | |
| var fn = functions[valueIndex]; | |
| return serializeFunc(fn, options); | |
| }); | |
| } |