2021-08-27 09:25:28 +00:00
|
|
|
import { DataTree } from "entities/DataTree/dataTreeFactory";
|
2021-03-13 14:12:21 +00:00
|
|
|
import {
|
2021-06-21 11:09:51 +00:00
|
|
|
EvaluationError,
|
2021-03-13 14:12:21 +00:00
|
|
|
extraLibraries,
|
2021-06-21 11:09:51 +00:00
|
|
|
PropertyEvaluationErrorType,
|
2021-03-13 14:12:21 +00:00
|
|
|
unsafeFunctionForEval,
|
|
|
|
|
} from "utils/DynamicBindingUtils";
|
|
|
|
|
import unescapeJS from "unescape-js";
|
2021-06-21 11:09:51 +00:00
|
|
|
import { JSHINT as jshint } from "jshint";
|
|
|
|
|
import { Severity } from "entities/AppsmithConsole";
|
2021-09-17 10:31:45 +00:00
|
|
|
import { Position } from "codemirror";
|
2021-08-27 09:25:28 +00:00
|
|
|
import { AppsmithPromise, enhanceDataTreeWithFunctions } from "./Actions";
|
|
|
|
|
import { ActionDescription } from "entities/DataTree/actionTriggers";
|
2021-09-17 10:31:45 +00:00
|
|
|
import { isEmpty, last } from "lodash";
|
2021-03-13 14:12:21 +00:00
|
|
|
|
|
|
|
|
export type EvalResult = {
|
|
|
|
|
result: any;
|
2021-08-27 09:25:28 +00:00
|
|
|
triggers?: ActionDescription[];
|
2021-06-21 11:09:51 +00:00
|
|
|
errors: EvaluationError[];
|
2021-03-13 14:12:21 +00:00
|
|
|
};
|
|
|
|
|
|
2021-06-21 11:09:51 +00:00
|
|
|
export enum EvaluationScriptType {
|
|
|
|
|
EXPRESSION = "EXPRESSION",
|
|
|
|
|
ANONYMOUS_FUNCTION = "ANONYMOUS_FUNCTION",
|
2021-07-07 09:59:44 +00:00
|
|
|
TRIGGERS = "TRIGGERS",
|
2021-06-21 11:09:51 +00:00
|
|
|
}
|
|
|
|
|
|
2021-09-17 10:31:45 +00:00
|
|
|
const evaluationScriptsPos: Record<EvaluationScriptType, string> = {
|
|
|
|
|
[EvaluationScriptType.EXPRESSION]: `
|
2021-07-15 04:59:04 +00:00
|
|
|
function closedFunction () {
|
2021-09-17 10:31:45 +00:00
|
|
|
const result = <<script>>
|
2021-07-15 04:59:04 +00:00
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
closedFunction()
|
|
|
|
|
`,
|
2021-09-17 10:31:45 +00:00
|
|
|
[EvaluationScriptType.ANONYMOUS_FUNCTION]: `
|
2021-07-15 04:59:04 +00:00
|
|
|
function callback (script) {
|
|
|
|
|
const userFunction = script;
|
|
|
|
|
const result = userFunction.apply(self, ARGUMENTS);
|
|
|
|
|
return result;
|
|
|
|
|
}
|
2021-09-17 10:31:45 +00:00
|
|
|
callback(<<script>>)
|
2021-07-15 04:59:04 +00:00
|
|
|
`,
|
2021-09-17 10:31:45 +00:00
|
|
|
[EvaluationScriptType.TRIGGERS]: `
|
2021-07-15 04:59:04 +00:00
|
|
|
function closedFunction () {
|
2021-09-17 10:31:45 +00:00
|
|
|
const result = <<script>>
|
|
|
|
|
return result
|
2021-07-15 04:59:04 +00:00
|
|
|
}
|
2021-08-27 09:25:28 +00:00
|
|
|
closedFunction();
|
2021-07-15 04:59:04 +00:00
|
|
|
`,
|
2021-06-21 11:09:51 +00:00
|
|
|
};
|
|
|
|
|
|
2021-09-17 10:31:45 +00:00
|
|
|
const getPositionInEvaluationScript = (
|
|
|
|
|
type: EvaluationScriptType,
|
|
|
|
|
): Position => {
|
|
|
|
|
const script = evaluationScriptsPos[type];
|
|
|
|
|
|
|
|
|
|
const index = script.indexOf("<<script>>");
|
|
|
|
|
const substr = script.substr(0, index);
|
|
|
|
|
const lines = substr.split("\n");
|
|
|
|
|
const lastLine = last(lines) || "";
|
|
|
|
|
|
|
|
|
|
return { line: lines.length, ch: lastLine.length };
|
|
|
|
|
};
|
|
|
|
|
|
2021-08-26 04:45:17 +00:00
|
|
|
const getScriptType = (
|
2021-06-21 11:09:51 +00:00
|
|
|
evalArguments?: Array<any>,
|
2021-07-07 09:59:44 +00:00
|
|
|
isTriggerBased = false,
|
2021-08-26 04:45:17 +00:00
|
|
|
): EvaluationScriptType => {
|
2021-07-07 09:59:44 +00:00
|
|
|
let scriptType = EvaluationScriptType.EXPRESSION;
|
|
|
|
|
if (evalArguments) {
|
|
|
|
|
scriptType = EvaluationScriptType.ANONYMOUS_FUNCTION;
|
|
|
|
|
} else if (isTriggerBased) {
|
|
|
|
|
scriptType = EvaluationScriptType.TRIGGERS;
|
|
|
|
|
}
|
2021-08-26 04:45:17 +00:00
|
|
|
return scriptType;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const getScriptToEval = (
|
|
|
|
|
userScript: string,
|
2021-09-17 10:31:45 +00:00
|
|
|
type: EvaluationScriptType,
|
2021-08-26 04:45:17 +00:00
|
|
|
): string => {
|
2021-09-17 10:31:45 +00:00
|
|
|
return evaluationScriptsPos[type].replace("<<script>>", userScript);
|
2021-06-21 11:09:51 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const getLintingErrors = (
|
|
|
|
|
script: string,
|
|
|
|
|
data: Record<string, unknown>,
|
2021-08-26 04:45:17 +00:00
|
|
|
originalBinding: string,
|
2021-09-17 10:31:45 +00:00
|
|
|
scriptPos: Position,
|
2021-06-21 11:09:51 +00:00
|
|
|
): EvaluationError[] => {
|
|
|
|
|
const globalData: Record<string, boolean> = {};
|
2021-09-17 10:31:45 +00:00
|
|
|
Object.keys(data).forEach((datum) => (globalData[datum] = true));
|
|
|
|
|
// Jshint shouldn't throw errors for additional libraries
|
|
|
|
|
extraLibraries.forEach((lib) => (globalData[lib.accessor] = true));
|
|
|
|
|
|
|
|
|
|
globalData.console = true;
|
|
|
|
|
|
2021-06-21 11:09:51 +00:00
|
|
|
const options = {
|
|
|
|
|
indent: 2,
|
2021-09-17 10:31:45 +00:00
|
|
|
esversion: 8, // For async/await support
|
|
|
|
|
eqeqeq: false, // Not necessary to use ===
|
|
|
|
|
curly: false, // Blocks can be added without {}, eg if (x) return true
|
|
|
|
|
freeze: true, // Overriding inbuilt classes like Array is not allowed
|
|
|
|
|
undef: true, // Undefined variables should be reported as error
|
|
|
|
|
forin: false, // Doesn't require filtering for..in loops with obj.hasOwnProperty()
|
|
|
|
|
noempty: false, // Empty blocks are allowed
|
|
|
|
|
strict: false, // We won't force strict mode
|
|
|
|
|
unused: false, // Unused variables are allowed
|
|
|
|
|
asi: true, // Tolerate Automatic Semicolon Insertion (no semicolons)
|
|
|
|
|
boss: true, // Tolerate assignments where comparisons would be expected
|
|
|
|
|
evil: false, // Use of eval not allowed
|
2021-09-22 07:05:02 +00:00
|
|
|
sub: true, // Don't force dot notation
|
2021-09-17 10:31:45 +00:00
|
|
|
funcscope: true, // Tolerate variable definition inside control statements
|
|
|
|
|
// environments
|
|
|
|
|
browser: true,
|
2021-06-21 11:09:51 +00:00
|
|
|
worker: true,
|
2021-09-17 10:31:45 +00:00
|
|
|
mocha: false,
|
|
|
|
|
// global values
|
2021-06-21 11:09:51 +00:00
|
|
|
globals: globalData,
|
|
|
|
|
};
|
2021-09-17 10:31:45 +00:00
|
|
|
|
2021-06-21 11:09:51 +00:00
|
|
|
jshint(script, options);
|
|
|
|
|
|
|
|
|
|
return jshint.errors.map((lintError) => {
|
2021-09-17 10:31:45 +00:00
|
|
|
const ch = lintError.character;
|
2021-06-21 11:09:51 +00:00
|
|
|
return {
|
|
|
|
|
errorType: PropertyEvaluationErrorType.LINT,
|
|
|
|
|
raw: script,
|
2021-09-17 10:31:45 +00:00
|
|
|
// We are forcing warnings to errors and removing unwanted JSHint checks
|
|
|
|
|
severity: Severity.ERROR,
|
2021-06-21 11:09:51 +00:00
|
|
|
errorMessage: lintError.reason,
|
|
|
|
|
errorSegment: lintError.evidence,
|
2021-08-26 04:45:17 +00:00
|
|
|
originalBinding,
|
|
|
|
|
// By keeping track of these variables we can highlight the exact text that caused the error.
|
|
|
|
|
variables: [lintError.a, lintError.b, lintError.c, lintError.d],
|
|
|
|
|
code: lintError.code,
|
2021-09-17 10:31:45 +00:00
|
|
|
line: lintError.line - scriptPos.line,
|
|
|
|
|
ch: lintError.line === scriptPos.line ? ch - scriptPos.ch : ch,
|
2021-06-21 11:09:51 +00:00
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const beginsWithLineBreakRegex = /^\s+|\s+$/;
|
|
|
|
|
|
2021-03-13 14:12:21 +00:00
|
|
|
export default function evaluate(
|
|
|
|
|
js: string,
|
|
|
|
|
data: DataTree,
|
2021-09-08 17:32:22 +00:00
|
|
|
resolvedFunctions: Record<string, any>,
|
2021-06-21 11:09:51 +00:00
|
|
|
evalArguments?: Array<any>,
|
2021-07-07 09:59:44 +00:00
|
|
|
isTriggerBased = false,
|
2021-03-13 14:12:21 +00:00
|
|
|
): EvalResult {
|
2021-06-21 11:09:51 +00:00
|
|
|
// We remove any line breaks from the beginning of the script because that
|
|
|
|
|
// makes the final function invalid. We also unescape any escaped characters
|
|
|
|
|
// so that eval can happen
|
|
|
|
|
const unescapedJS = unescapeJS(js.replace(beginsWithLineBreakRegex, ""));
|
2021-09-17 10:31:45 +00:00
|
|
|
const scriptType = getScriptType(evalArguments, isTriggerBased);
|
|
|
|
|
const script = getScriptToEval(unescapedJS, scriptType);
|
|
|
|
|
// We are linting original js binding,
|
|
|
|
|
// This will make sure that the characted count is not messed up when we do unescapejs
|
|
|
|
|
const scriptToLint = getScriptToEval(js, scriptType);
|
2021-06-21 11:09:51 +00:00
|
|
|
return (function() {
|
|
|
|
|
let errors: EvaluationError[] = [];
|
|
|
|
|
let result;
|
|
|
|
|
let triggers: any[] = [];
|
2021-03-13 14:12:21 +00:00
|
|
|
/**** Setting the eval context ****/
|
|
|
|
|
const GLOBAL_DATA: Record<string, any> = {};
|
|
|
|
|
///// Adding callback data
|
2021-06-21 11:09:51 +00:00
|
|
|
GLOBAL_DATA.ARGUMENTS = evalArguments;
|
2021-08-27 09:25:28 +00:00
|
|
|
GLOBAL_DATA.Promise = AppsmithPromise;
|
2021-07-13 12:57:59 +00:00
|
|
|
if (isTriggerBased) {
|
|
|
|
|
//// Add internal functions to dataTree;
|
2021-08-27 09:25:28 +00:00
|
|
|
const dataTreeWithFunctions = enhanceDataTreeWithFunctions(data);
|
2021-07-13 12:57:59 +00:00
|
|
|
///// Adding Data tree with functions
|
|
|
|
|
Object.keys(dataTreeWithFunctions).forEach((datum) => {
|
|
|
|
|
GLOBAL_DATA[datum] = dataTreeWithFunctions[datum];
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
///// Adding Data tree
|
|
|
|
|
Object.keys(data).forEach((datum) => {
|
|
|
|
|
GLOBAL_DATA[datum] = data[datum];
|
2021-03-13 14:12:21 +00:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Set it to self so that the eval function can have access to it
|
|
|
|
|
// as global data. This is what enables access all appsmith
|
|
|
|
|
// entity properties from the global context
|
2021-09-08 17:32:22 +00:00
|
|
|
|
2021-03-13 14:12:21 +00:00
|
|
|
Object.keys(GLOBAL_DATA).forEach((key) => {
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
|
|
|
// @ts-ignore: No types available
|
|
|
|
|
self[key] = GLOBAL_DATA[key];
|
|
|
|
|
});
|
2021-09-08 17:32:22 +00:00
|
|
|
|
|
|
|
|
if (!isEmpty(resolvedFunctions)) {
|
|
|
|
|
Object.keys(resolvedFunctions).forEach((datum: any) => {
|
|
|
|
|
const resolvedObject = resolvedFunctions[datum];
|
|
|
|
|
Object.keys(resolvedObject).forEach((key: any) => {
|
|
|
|
|
self[datum][key] = resolvedObject[key];
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
2021-09-17 10:31:45 +00:00
|
|
|
errors = getLintingErrors(
|
|
|
|
|
scriptToLint,
|
|
|
|
|
GLOBAL_DATA,
|
|
|
|
|
js,
|
|
|
|
|
getPositionInEvaluationScript(scriptType),
|
|
|
|
|
);
|
2021-03-13 14:12:21 +00:00
|
|
|
|
|
|
|
|
///// Adding extra libraries separately
|
|
|
|
|
extraLibraries.forEach((library) => {
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
|
|
|
// @ts-ignore: No types available
|
|
|
|
|
self[library.accessor] = library.lib;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
///// Remove all unsafe functions
|
|
|
|
|
unsafeFunctionForEval.forEach((func) => {
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
|
|
|
// @ts-ignore: No types available
|
|
|
|
|
self[func] = undefined;
|
|
|
|
|
});
|
2021-06-21 11:09:51 +00:00
|
|
|
try {
|
2021-07-15 04:59:04 +00:00
|
|
|
result = eval(script);
|
2021-07-13 12:57:59 +00:00
|
|
|
if (isTriggerBased) {
|
|
|
|
|
triggers = [...self.triggers];
|
2021-08-27 09:25:28 +00:00
|
|
|
self.triggers = [];
|
2021-07-13 12:57:59 +00:00
|
|
|
}
|
2021-06-21 11:09:51 +00:00
|
|
|
} catch (e) {
|
2021-07-29 11:36:49 +00:00
|
|
|
const errorMessage = `${e.name}: ${e.message}`;
|
2021-06-21 11:09:51 +00:00
|
|
|
errors.push({
|
2021-07-29 11:36:49 +00:00
|
|
|
errorMessage: errorMessage,
|
2021-06-21 11:09:51 +00:00
|
|
|
severity: Severity.ERROR,
|
|
|
|
|
raw: script,
|
|
|
|
|
errorType: PropertyEvaluationErrorType.PARSE,
|
2021-08-26 04:45:17 +00:00
|
|
|
originalBinding: js,
|
2021-06-21 11:09:51 +00:00
|
|
|
});
|
|
|
|
|
}
|
2021-03-13 14:12:21 +00:00
|
|
|
|
2021-09-21 06:02:45 +00:00
|
|
|
if (!isEmpty(resolvedFunctions)) {
|
|
|
|
|
Object.keys(resolvedFunctions).forEach((datum: any) => {
|
|
|
|
|
const resolvedObject = resolvedFunctions[datum];
|
|
|
|
|
Object.keys(resolvedObject).forEach((key: any) => {
|
2021-10-01 17:10:49 +00:00
|
|
|
if (resolvedObject[key]) {
|
|
|
|
|
self[datum][key] = resolvedObject[key].toString();
|
|
|
|
|
}
|
2021-09-21 06:02:45 +00:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
2021-03-13 14:12:21 +00:00
|
|
|
// Remove it from self
|
|
|
|
|
// This is needed so that next eval can have a clean sheet
|
|
|
|
|
Object.keys(GLOBAL_DATA).forEach((key) => {
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
|
|
|
// @ts-ignore: No types available
|
|
|
|
|
delete self[key];
|
|
|
|
|
});
|
|
|
|
|
|
2021-06-21 11:09:51 +00:00
|
|
|
return { result, triggers, errors };
|
2021-03-13 14:12:21 +00:00
|
|
|
})();
|
|
|
|
|
}
|