|
12 | 12 | /* ******** VARIABLES ******** */
|
13 | 13 | /* ************************************* */
|
14 | 14 |
|
| 15 | +const basicFunctionPattern = new RegExp( |
| 16 | + // eslint-disable-next-line prefer-template |
| 17 | + '' |
| 18 | + + /^function/.source |
| 19 | + + / *([$_a-zA-Z][$\w]*)?/.source // name |
| 20 | + + / *\([ \n]*([$_a-zA-Z][$\w]*(?:[ \n]*,[ \n]*[$_a-zA-Z][$\w]*)*)*?,?[ \n]*\)/.source // params |
| 21 | + + /[ \n]*{\n*(.*?)\n? *}$/.source, // body |
| 22 | + 's', |
| 23 | +); |
15 | 24 |
|
16 | 25 | /* ************************************* */
|
17 | 26 | /* ******** PRIVATE FUNCTIONS ******** */
|
18 | 27 | /* ************************************* */
|
19 | 28 |
|
| 29 | +/** |
| 30 | + * Try to regex match a string as a javascript function. |
| 31 | + * @param functionString {string} string to match |
| 32 | + * @param splitParams {boolean} whether to split parameters into an array |
| 33 | + * @returns {{name: string, params: string | string[], body: string} | null} |
| 34 | + */ |
| 35 | +function matchFunction(functionString, splitParams = false) { |
| 36 | + const match = basicFunctionPattern.exec(functionString); |
| 37 | + if (match === null) return null; |
| 38 | + return { |
| 39 | + name: match[1], |
| 40 | + params: splitParams ? commaSplit(match[2]) : match[2], |
| 41 | + body: match[3], |
| 42 | + }; |
| 43 | +} |
| 44 | + |
| 45 | +/** |
| 46 | + * Split comma separated strings and trim surrounding whitespace. |
| 47 | + * @param string {string | undefined} a string of comma-separated strings |
| 48 | + * @returns {string[]} an array of elements that were separated by commas with |
| 49 | + * surrounding whitespace trimmed. May be empty. |
| 50 | + */ |
| 51 | +function commaSplit(string) { |
| 52 | + if (!string) return []; |
| 53 | + return string.split(',').map(x => x.trim()); |
| 54 | +} |
| 55 | + |
| 56 | +/** |
| 57 | + * Try creating an anonymous function from a string, or return null if it's |
| 58 | + * not a valid function definition. |
| 59 | + * Note that this is not a completely safe, there are still security flaws, |
| 60 | + * but it is safer than using `exec`. |
| 61 | + * @param functionString {string} string to try to parse as a function |
| 62 | + * definition |
| 63 | + * @returns {Function | null} an anonymous function if the string is a valid |
| 64 | + * function definition, else null |
| 65 | + */ |
| 66 | +function createFunction(functionString) { |
| 67 | + /* This is not an exhaustive check by any means |
| 68 | + * For instance, function names may have a wide variety of |
| 69 | + * unicode characters and still be valid... oh well! |
| 70 | + * |
| 71 | + * TEST CASES: |
| 72 | + * |
| 73 | + * // Should match (single-line): |
| 74 | + * function() {} |
| 75 | + * function () {} |
| 76 | + * function myFunc(){} |
| 77 | + * function myFunc(arg1){} |
| 78 | + * function(arg1,arg2, arg3, arg4) {} |
| 79 | + * function myFunc(arg1, arg2, arg3){} |
| 80 | + * function myFunc(arg1, arg2, arg3){console.log('something');} |
| 81 | + * function myFunc(arg1,){} |
| 82 | + * function myFunc(arg1, ){} |
| 83 | + * function myFunc(arg1) {if (true) {var moreCurlyBraces = 1;}} |
| 84 | + * |
| 85 | + * // Should match (multi-line): |
| 86 | + * function myFunc(arg1, arg2, arg3) { |
| 87 | + * console.log('something'); |
| 88 | + * } |
| 89 | + * |
| 90 | + * function myFunc() { |
| 91 | + * console.log('test'); |
| 92 | + * if (true) { |
| 93 | + * console.log('test2'); |
| 94 | + * } |
| 95 | + * } |
| 96 | + * |
| 97 | + * // Should not match (single-line): |
| 98 | + * anotherFunction() |
| 99 | + * function myFunc {} |
| 100 | + * function myFunc()); (anotherFunction() |
| 101 | + * function myFunc(){}, anotherFunction() |
| 102 | + */ |
| 103 | + const match = matchFunction(functionString, true); |
| 104 | + if (!match) return null; |
| 105 | + |
| 106 | + // Here's the security flaw. We want this functionality for supporting |
| 107 | + // JSONP, so we've opted for the best attempt at maintaining some amount |
| 108 | + // of security. This should be a little better than eval because it |
| 109 | + // shouldn't automatically execute code, just create a function which can |
| 110 | + // be called later. |
| 111 | + // eslint-disable-next-line no-new-func |
| 112 | + const func = new Function(...match.params, match.body || ''); |
| 113 | + func.displayName = match.name; |
| 114 | + return func; |
| 115 | +} |
| 116 | + |
20 | 117 |
|
21 | 118 | /* ************************************* */
|
22 | 119 | /* ******** PUBLIC FUNCTIONS ******** */
|
23 | 120 | /* ************************************* */
|
24 | 121 |
|
25 | 122 | /**
|
26 |
| - * Parse. |
27 |
| - * @param string {String} string to parse |
28 |
| - * @returns {*} |
| 123 | + * Parse a string into either a function or a JSON element. |
| 124 | + * @param string {string} string to parse |
| 125 | + * @param allowFunctionEvaluation {boolean} whether to parse strings that |
| 126 | + * are function definitions as Javascript |
| 127 | + * @returns {Function | Object | Array | null | boolean | number | string} |
29 | 128 | */
|
30 |
| -function parse(string) { |
31 |
| - let result = string; |
32 |
| - |
33 |
| - // Check if string contains 'function' and start with it to eval it |
34 |
| - if (result.indexOf('function') === 0) { |
35 |
| - return eval(`(${result})`); // eslint-disable-line no-eval |
| 129 | +function parse(string, allowFunctionEvaluation) { |
| 130 | + // Try parsing (and sanitizing) a function |
| 131 | + if (allowFunctionEvaluation) { |
| 132 | + const func = createFunction(string); |
| 133 | + if (func !== null) return func; |
36 | 134 | }
|
37 | 135 |
|
38 | 136 | try {
|
39 |
| - result = JSON.parse(string); |
| 137 | + return JSON.parse(string); |
40 | 138 | } catch (e) {
|
41 |
| - // Error |
| 139 | + return string; |
| 140 | + } |
| 141 | +} |
| 142 | + |
| 143 | +/** |
| 144 | + * A different implementation of Function.prototype.toString which tries to get |
| 145 | + * a function name using displayName when name is "anonymous". |
| 146 | + * @param func {Function} function to transform into a string |
| 147 | + * @returns {string} a string representing the function |
| 148 | + */ |
| 149 | +function functionToString(func) { |
| 150 | + const pattern = /^function anonymous/; |
| 151 | + const funcStr = func.toString(); |
| 152 | + if (pattern.test(funcStr) && func.displayName) { |
| 153 | + return func.toString().replace(pattern, `function ${func.displayName}`); |
42 | 154 | }
|
43 |
| - return result; |
| 155 | + return funcStr; |
44 | 156 | }
|
45 | 157 |
|
46 | 158 | /* ************************************* */
|
47 | 159 | /* ******** EXPORTS ******** */
|
48 | 160 | /* ************************************* */
|
49 | 161 | export default parse;
|
| 162 | +export { functionToString }; |
0 commit comments