2021-12-23 14:17:20 +00:00
|
|
|
/* eslint-disable no-console */
|
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 { Severity } from "entities/AppsmithConsole";
|
2021-12-23 14:17:20 +00:00
|
|
|
import { enhanceDataTreeWithFunctions } from "./Actions";
|
2021-10-05 13:52:27 +00:00
|
|
|
import { isEmpty } from "lodash";
|
|
|
|
|
import { getLintingErrors } from "workers/lint";
|
2021-12-23 14:17:20 +00:00
|
|
|
import { completePromise } from "workers/PromisifyAction";
|
|
|
|
|
import { ActionDescription } from "entities/DataTree/actionTriggers";
|
2021-03-13 14:12:21 +00:00
|
|
|
|
|
|
|
|
export type EvalResult = {
|
|
|
|
|
result: any;
|
2021-06-21 11:09:51 +00:00
|
|
|
errors: EvaluationError[];
|
2021-12-23 14:17:20 +00:00
|
|
|
triggers?: ActionDescription[];
|
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-10-07 12:33:15 +00:00
|
|
|
export const ScriptTemplate = "<<string>>";
|
|
|
|
|
|
2021-10-05 13:52:27 +00:00
|
|
|
export const EvaluationScripts: Record<EvaluationScriptType, string> = {
|
2021-09-17 10:31:45 +00:00
|
|
|
[EvaluationScriptType.EXPRESSION]: `
|
2021-07-15 04:59:04 +00:00
|
|
|
function closedFunction () {
|
2021-10-07 12:33:15 +00:00
|
|
|
const result = ${ScriptTemplate}
|
2021-07-15 04:59:04 +00:00
|
|
|
return result;
|
|
|
|
|
}
|
2021-12-14 08:30:43 +00:00
|
|
|
closedFunction.call(THIS_CONTEXT)
|
2021-07-15 04:59:04 +00:00
|
|
|
`,
|
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;
|
2021-12-21 14:30:19 +00:00
|
|
|
const result = userFunction?.apply(THIS_CONTEXT, ARGUMENTS);
|
2021-07-15 04:59:04 +00:00
|
|
|
return result;
|
|
|
|
|
}
|
2021-10-07 12:33:15 +00:00
|
|
|
callback(${ScriptTemplate})
|
2021-07-15 04:59:04 +00:00
|
|
|
`,
|
2021-09-17 10:31:45 +00:00
|
|
|
[EvaluationScriptType.TRIGGERS]: `
|
2021-12-23 14:17:20 +00:00
|
|
|
async function closedFunction () {
|
|
|
|
|
const result = await ${ScriptTemplate};
|
|
|
|
|
return result;
|
2021-07-15 04:59:04 +00:00
|
|
|
}
|
2021-12-14 08:30:43 +00:00
|
|
|
closedFunction.call(THIS_CONTEXT);
|
2021-07-15 04:59:04 +00:00
|
|
|
`,
|
2021-06-21 11:09:51 +00:00
|
|
|
};
|
|
|
|
|
|
2021-08-26 04:45:17 +00:00
|
|
|
const getScriptType = (
|
2021-12-23 14:17:20 +00:00
|
|
|
evalArgumentsExist = false,
|
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;
|
2021-12-23 14:17:20 +00:00
|
|
|
if (evalArgumentsExist) {
|
2021-07-07 09:59:44 +00:00
|
|
|
scriptType = EvaluationScriptType.ANONYMOUS_FUNCTION;
|
|
|
|
|
} else if (isTriggerBased) {
|
|
|
|
|
scriptType = EvaluationScriptType.TRIGGERS;
|
|
|
|
|
}
|
2021-08-26 04:45:17 +00:00
|
|
|
return scriptType;
|
|
|
|
|
};
|
|
|
|
|
|
2021-10-05 13:52:27 +00:00
|
|
|
export const getScriptToEval = (
|
2021-08-26 04:45:17 +00:00
|
|
|
userScript: string,
|
2021-09-17 10:31:45 +00:00
|
|
|
type: EvaluationScriptType,
|
2021-08-26 04:45:17 +00:00
|
|
|
): string => {
|
2021-10-07 12:33:15 +00:00
|
|
|
// Using replace here would break scripts with replacement patterns (ex: $&, $$)
|
|
|
|
|
const buffer = EvaluationScripts[type].split(ScriptTemplate);
|
|
|
|
|
return `${buffer[0]}${userScript}${buffer[1]}`;
|
2021-06-21 11:09:51 +00:00
|
|
|
};
|
|
|
|
|
|
2021-10-11 12:55:03 +00:00
|
|
|
export function setupEvaluationEnvironment() {
|
|
|
|
|
///// 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-10-05 13:52:27 +00:00
|
|
|
const beginsWithLineBreakRegex = /^\s+|\s+$/;
|
2021-06-21 11:09:51 +00:00
|
|
|
|
2021-10-05 13:52:27 +00:00
|
|
|
export const createGlobalData = (
|
|
|
|
|
dataTree: DataTree,
|
|
|
|
|
resolvedFunctions: Record<string, any>,
|
2021-12-14 08:30:43 +00:00
|
|
|
context?: EvaluateContext,
|
2021-10-05 13:52:27 +00:00
|
|
|
evalArguments?: Array<any>,
|
|
|
|
|
) => {
|
|
|
|
|
const GLOBAL_DATA: Record<string, any> = {};
|
|
|
|
|
///// Adding callback data
|
|
|
|
|
GLOBAL_DATA.ARGUMENTS = evalArguments;
|
2021-12-14 08:30:43 +00:00
|
|
|
//// Adding contextual data not part of data tree
|
|
|
|
|
GLOBAL_DATA.THIS_CONTEXT = {};
|
|
|
|
|
if (context) {
|
|
|
|
|
if (context.thisContext) {
|
|
|
|
|
GLOBAL_DATA.THIS_CONTEXT = context.thisContext;
|
|
|
|
|
}
|
|
|
|
|
if (context.globalContext) {
|
|
|
|
|
Object.entries(context.globalContext).forEach(([key, value]) => {
|
|
|
|
|
GLOBAL_DATA[key] = value;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
2021-12-23 14:17:20 +00:00
|
|
|
//// Add internal functions to dataTree;
|
|
|
|
|
const dataTreeWithFunctions = enhanceDataTreeWithFunctions(
|
|
|
|
|
dataTree,
|
|
|
|
|
context?.requestId,
|
|
|
|
|
);
|
|
|
|
|
///// Adding Data tree with functions
|
|
|
|
|
Object.keys(dataTreeWithFunctions).forEach((datum) => {
|
|
|
|
|
GLOBAL_DATA[datum] = dataTreeWithFunctions[datum];
|
|
|
|
|
});
|
2021-10-05 13:52:27 +00:00
|
|
|
if (!isEmpty(resolvedFunctions)) {
|
|
|
|
|
Object.keys(resolvedFunctions).forEach((datum: any) => {
|
|
|
|
|
const resolvedObject = resolvedFunctions[datum];
|
|
|
|
|
Object.keys(resolvedObject).forEach((key: any) => {
|
2021-10-12 10:19:10 +00:00
|
|
|
const dataTreeKey = GLOBAL_DATA[datum];
|
|
|
|
|
if (dataTreeKey) {
|
|
|
|
|
dataTreeKey[key] = resolvedObject[key];
|
|
|
|
|
}
|
2021-10-05 13:52:27 +00:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
return GLOBAL_DATA;
|
2021-06-21 11:09:51 +00:00
|
|
|
};
|
|
|
|
|
|
2021-12-14 08:30:43 +00:00
|
|
|
export function sanitizeScript(js: string) {
|
2021-12-02 10:03:43 +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 trimmedJS = js.replace(beginsWithLineBreakRegex, "");
|
2021-12-14 08:30:43 +00:00
|
|
|
return self.evaluationVersion > 1 ? trimmedJS : unescapeJS(trimmedJS);
|
2021-12-02 10:03:43 +00:00
|
|
|
}
|
|
|
|
|
|
2021-12-14 08:30:43 +00:00
|
|
|
/** Define a context just for this script
|
|
|
|
|
* thisContext will define it on the `this`
|
|
|
|
|
* globalContext will define it globally
|
|
|
|
|
*/
|
|
|
|
|
export type EvaluateContext = {
|
|
|
|
|
thisContext?: Record<string, any>;
|
|
|
|
|
globalContext?: Record<string, any>;
|
2021-12-23 14:17:20 +00:00
|
|
|
requestId?: string;
|
2021-12-14 08:30:43 +00:00
|
|
|
};
|
|
|
|
|
|
2021-12-23 14:17:20 +00:00
|
|
|
export const getUserScriptToEvaluate = (
|
|
|
|
|
userScript: string,
|
|
|
|
|
GLOBAL_DATA: Record<string, unknown>,
|
|
|
|
|
isTriggerBased: boolean,
|
2021-06-21 11:09:51 +00:00
|
|
|
evalArguments?: Array<any>,
|
2021-12-23 14:17:20 +00:00
|
|
|
) => {
|
|
|
|
|
const unescapedJS = sanitizeScript(userScript);
|
|
|
|
|
// If nothing is present to evaluate, return instead of linting
|
|
|
|
|
if (!unescapedJS.length) {
|
2021-12-21 14:30:19 +00:00
|
|
|
return {
|
2021-12-23 14:17:20 +00:00
|
|
|
lintErrors: [],
|
|
|
|
|
script: "",
|
2021-12-21 14:30:19 +00:00
|
|
|
};
|
|
|
|
|
}
|
2021-12-23 14:17:20 +00:00
|
|
|
const scriptType = getScriptType(!!evalArguments, isTriggerBased);
|
|
|
|
|
const script = getScriptToEval(unescapedJS, scriptType);
|
2021-09-17 10:31:45 +00:00
|
|
|
// We are linting original js binding,
|
2021-10-05 13:52:27 +00:00
|
|
|
// This will make sure that the character count is not messed up when we do unescapejs
|
2021-12-23 14:17:20 +00:00
|
|
|
const scriptToLint = getScriptToEval(userScript, scriptType);
|
|
|
|
|
const lintErrors = getLintingErrors(
|
|
|
|
|
scriptToLint,
|
|
|
|
|
GLOBAL_DATA,
|
|
|
|
|
userScript,
|
|
|
|
|
scriptType,
|
|
|
|
|
);
|
|
|
|
|
return { script, lintErrors };
|
|
|
|
|
};
|
2021-12-21 14:30:19 +00:00
|
|
|
|
2021-12-23 14:17:20 +00:00
|
|
|
export default function evaluateSync(
|
|
|
|
|
userScript: string,
|
|
|
|
|
dataTree: DataTree,
|
|
|
|
|
resolvedFunctions: Record<string, any>,
|
|
|
|
|
context?: EvaluateContext,
|
|
|
|
|
evalArguments?: Array<any>,
|
|
|
|
|
): EvalResult {
|
2021-06-21 11:09:51 +00:00
|
|
|
return (function() {
|
|
|
|
|
let errors: EvaluationError[] = [];
|
|
|
|
|
let result;
|
2021-03-13 14:12:21 +00:00
|
|
|
/**** Setting the eval context ****/
|
2021-10-05 13:52:27 +00:00
|
|
|
const GLOBAL_DATA: Record<string, any> = createGlobalData(
|
2021-12-23 14:17:20 +00:00
|
|
|
dataTree,
|
2021-10-05 13:52:27 +00:00
|
|
|
resolvedFunctions,
|
2021-12-14 08:30:43 +00:00
|
|
|
context,
|
2021-10-05 13:52:27 +00:00
|
|
|
evalArguments,
|
|
|
|
|
);
|
2021-12-23 14:17:20 +00:00
|
|
|
GLOBAL_DATA.ALLOW_ASYNC = false;
|
|
|
|
|
const { lintErrors, script } = getUserScriptToEvaluate(
|
|
|
|
|
userScript,
|
|
|
|
|
GLOBAL_DATA,
|
|
|
|
|
false,
|
|
|
|
|
evalArguments,
|
|
|
|
|
);
|
|
|
|
|
// If nothing is present to evaluate, return instead of evaluating
|
|
|
|
|
if (!script.length) {
|
|
|
|
|
return {
|
|
|
|
|
errors: [],
|
|
|
|
|
result: undefined,
|
|
|
|
|
triggers: [],
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
errors = lintErrors;
|
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-10-05 13:52:27 +00:00
|
|
|
for (const entity in GLOBAL_DATA) {
|
2021-03-13 14:12:21 +00:00
|
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
|
|
|
// @ts-ignore: No types available
|
2021-10-05 13:52:27 +00:00
|
|
|
self[entity] = GLOBAL_DATA[entity];
|
2021-09-08 17:32:22 +00:00
|
|
|
}
|
2021-03-13 14:12:21 +00:00
|
|
|
|
2021-06-21 11:09:51 +00:00
|
|
|
try {
|
2021-07-15 04:59:04 +00:00
|
|
|
result = eval(script);
|
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-12-23 14:17:20 +00:00
|
|
|
originalBinding: userScript,
|
2021-06-21 11:09:51 +00:00
|
|
|
});
|
2021-12-23 14:17:20 +00:00
|
|
|
} finally {
|
|
|
|
|
for (const entity in GLOBAL_DATA) {
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
|
|
|
// @ts-ignore: No types available
|
|
|
|
|
delete self[entity];
|
|
|
|
|
}
|
2021-06-21 11:09:51 +00:00
|
|
|
}
|
2021-03-13 14:12:21 +00:00
|
|
|
|
2021-12-23 14:17:20 +00:00
|
|
|
return { result, errors };
|
|
|
|
|
})();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function evaluateAsync(
|
|
|
|
|
userScript: string,
|
|
|
|
|
dataTree: DataTree,
|
|
|
|
|
requestId: string,
|
|
|
|
|
resolvedFunctions: Record<string, any>,
|
|
|
|
|
context?: EvaluateContext,
|
|
|
|
|
evalArguments?: Array<any>,
|
|
|
|
|
) {
|
|
|
|
|
return (async function() {
|
|
|
|
|
const errors: EvaluationError[] = [];
|
|
|
|
|
let result;
|
|
|
|
|
/**** Setting the eval context ****/
|
|
|
|
|
const GLOBAL_DATA: Record<string, any> = createGlobalData(
|
|
|
|
|
dataTree,
|
|
|
|
|
resolvedFunctions,
|
|
|
|
|
{ ...context, requestId },
|
|
|
|
|
evalArguments,
|
|
|
|
|
);
|
|
|
|
|
const { script } = getUserScriptToEvaluate(
|
|
|
|
|
userScript,
|
|
|
|
|
GLOBAL_DATA,
|
|
|
|
|
true,
|
|
|
|
|
evalArguments,
|
|
|
|
|
);
|
|
|
|
|
GLOBAL_DATA.ALLOW_ASYNC = true;
|
|
|
|
|
// 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
|
|
|
|
|
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];
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
result = await eval(script);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
const errorMessage = `UncaughtPromiseRejection: ${error.message}`;
|
|
|
|
|
errors.push({
|
|
|
|
|
errorMessage: errorMessage,
|
|
|
|
|
severity: Severity.ERROR,
|
|
|
|
|
raw: script,
|
|
|
|
|
errorType: PropertyEvaluationErrorType.PARSE,
|
|
|
|
|
originalBinding: userScript,
|
|
|
|
|
});
|
|
|
|
|
} finally {
|
|
|
|
|
completePromise(requestId, {
|
|
|
|
|
result,
|
|
|
|
|
errors,
|
|
|
|
|
triggers: Array.from(self.TRIGGER_COLLECTOR),
|
2021-09-21 06:02:45 +00:00
|
|
|
});
|
2021-12-23 14:17:20 +00:00
|
|
|
for (const entity in GLOBAL_DATA) {
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
|
|
|
// @ts-ignore: No types available
|
|
|
|
|
delete self[entity];
|
|
|
|
|
}
|
2021-09-21 06:02:45 +00:00
|
|
|
}
|
2021-12-23 14:17:20 +00:00
|
|
|
})();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function isFunctionAsync(userFunction: unknown, dataTree: DataTree) {
|
|
|
|
|
return (function() {
|
|
|
|
|
/**** Setting the eval context ****/
|
|
|
|
|
const GLOBAL_DATA: Record<string, any> = {
|
|
|
|
|
ALLOW_ASYNC: false,
|
|
|
|
|
IS_ASYNC: false,
|
|
|
|
|
};
|
|
|
|
|
//// Add internal functions to dataTree;
|
|
|
|
|
const dataTreeWithFunctions = enhanceDataTreeWithFunctions(dataTree);
|
|
|
|
|
///// Adding Data tree with functions
|
|
|
|
|
Object.keys(dataTreeWithFunctions).forEach((datum) => {
|
|
|
|
|
GLOBAL_DATA[datum] = dataTreeWithFunctions[datum];
|
|
|
|
|
});
|
|
|
|
|
// 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-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
|
2021-12-23 14:17:20 +00:00
|
|
|
self[key] = GLOBAL_DATA[key];
|
2021-03-13 14:12:21 +00:00
|
|
|
});
|
2021-12-23 14:17:20 +00:00
|
|
|
try {
|
|
|
|
|
if (typeof userFunction === "function") {
|
|
|
|
|
const returnValue = userFunction();
|
|
|
|
|
if (!!returnValue && returnValue instanceof Promise) {
|
|
|
|
|
self.IS_ASYNC = true;
|
|
|
|
|
}
|
|
|
|
|
if (self.TRIGGER_COLLECTOR.length) {
|
|
|
|
|
self.IS_ASYNC = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error("Error when determining async function", e);
|
|
|
|
|
}
|
|
|
|
|
const isAsync = !!self.IS_ASYNC;
|
|
|
|
|
for (const entity in GLOBAL_DATA) {
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
|
|
|
// @ts-ignore: No types available
|
|
|
|
|
delete self[entity];
|
|
|
|
|
}
|
|
|
|
|
return isAsync;
|
2021-03-13 14:12:21 +00:00
|
|
|
})();
|
|
|
|
|
}
|