diff --git a/app/client/src/constants/AppsmithActionConstants/ActionConstants.tsx b/app/client/src/constants/AppsmithActionConstants/ActionConstants.tsx index b7368bdeb5..b76f6b6dec 100644 --- a/app/client/src/constants/AppsmithActionConstants/ActionConstants.tsx +++ b/app/client/src/constants/AppsmithActionConstants/ActionConstants.tsx @@ -113,7 +113,7 @@ export interface ExecuteErrorPayload extends ErrorActionPayload { export const urlGroupsRegexExp = /^(https?:\/{2}\S+?)(\/[\s\S]*?)(\?(?![^{]*})[\s\S]*)?$/; export const EXECUTION_PARAM_KEY = "executionParams"; -export const EXECUTION_PARAM_REFERENCE_REGEX = /this.params/g; +export const THIS_DOT_PARAMS_KEY = "params"; export const RESP_HEADER_DATATYPE = "X-APPSMITH-DATATYPE"; export const API_REQUEST_HEADERS: APIHeaders = { diff --git a/app/client/src/workers/DataTreeEvaluator.ts b/app/client/src/workers/DataTreeEvaluator.ts index bc64f138f2..ba6cd038fc 100644 --- a/app/client/src/workers/DataTreeEvaluator.ts +++ b/app/client/src/workers/DataTreeEvaluator.ts @@ -53,12 +53,13 @@ import toposort from "toposort"; import equal from "fast-deep-equal/es6"; import { EXECUTION_PARAM_KEY, - EXECUTION_PARAM_REFERENCE_REGEX, + THIS_DOT_PARAMS_KEY, } from "constants/AppsmithActionConstants/ActionConstants"; import { DATA_BIND_REGEX } from "constants/BindingsConstants"; import evaluate, { createGlobalData, EvalResult, + EvaluateContext, EvaluationScriptType, getScriptToEval, } from "workers/evaluate"; @@ -584,6 +585,13 @@ export default class DataTreeEvaluator { const evaluationSubstitutionType = entity.bindingPaths[propertyPath] || EvaluationSubstitutionType.TEMPLATE; + + const contextData: EvaluateContext = {}; + if (isAction(entity)) { + contextData.thisContext = { + params: {}, + }; + } try { evalPropertyValue = this.getDynamicValue( unEvalPropertyValue, @@ -591,6 +599,7 @@ export default class DataTreeEvaluator { resolvedFunctions, evaluationSubstitutionType, false, + contextData, undefined, fullPropertyPath, ); @@ -757,6 +766,7 @@ export default class DataTreeEvaluator { resolvedFunctions: Record, evaluationSubstitutionType: EvaluationSubstitutionType, returnTriggers: boolean, + contextData?: EvaluateContext, callBackData?: Array, fullPropertyPath?: string, ) { @@ -777,6 +787,7 @@ export default class DataTreeEvaluator { jsSnippets[0], data, resolvedFunctions, + contextData, callBackData, returnTriggers, ); @@ -793,6 +804,7 @@ export default class DataTreeEvaluator { toBeSentForEval, data, resolvedFunctions, + contextData, callBackData, entity && isJSAction(entity), ); @@ -848,6 +860,7 @@ export default class DataTreeEvaluator { js: string, data: DataTree, resolvedFunctions: Record, + contextData?: EvaluateContext, callbackData?: Array, isTriggerBased = false, ): EvalResult { @@ -856,6 +869,7 @@ export default class DataTreeEvaluator { js, data, resolvedFunctions, + contextData, callbackData, isTriggerBased, ); @@ -894,6 +908,7 @@ export default class DataTreeEvaluator { EvaluationSubstitutionType.TEMPLATE, true, undefined, + undefined, fullPropertyPath, ); valueToValidate = triggers; @@ -1532,23 +1547,18 @@ export default class DataTreeEvaluator { ); } - // Replace any reference of 'this.params' to 'executionParams' (backwards compatibility) - const bindingsForExecutionParams: string[] = bindings.map( - (binding: string) => - binding.replace(EXECUTION_PARAM_REFERENCE_REGEX, EXECUTION_PARAM_KEY), - ); - - const dataTreeWithExecutionParams = Object.assign({}, this.evalTree, { - [EXECUTION_PARAM_KEY]: evaluatedExecutionParams, - }); - - return bindingsForExecutionParams.map((binding) => + return bindings.map((binding) => this.getDynamicValue( `{{${binding}}}`, - dataTreeWithExecutionParams, + this.evalTree, this.resolvedFunctions, EvaluationSubstitutionType.TEMPLATE, false, + // params can be accessed via "this.params" or "executionParams" + { + thisContext: { [THIS_DOT_PARAMS_KEY]: evaluatedExecutionParams }, + globalContext: { [EXECUTION_PARAM_KEY]: evaluatedExecutionParams }, + }, ), ); } diff --git a/app/client/src/workers/ast.ts b/app/client/src/workers/ast.ts index 28156caebb..3e4da609f7 100644 --- a/app/client/src/workers/ast.ts +++ b/app/client/src/workers/ast.ts @@ -2,7 +2,7 @@ import { parse, Node } from "acorn"; import { ancestor } from "acorn-walk"; import _ from "lodash"; import { ECMA_VERSION } from "workers/constants"; -import { unEscapeScript } from "./evaluate"; +import { sanitizeScript } from "./evaluate"; /* * Valuable links: @@ -153,17 +153,17 @@ export const extractIdentifiersFromCode = (code: string): string[] => { let functionalParams = new Set(); let ast: Node = { end: 0, start: 0, type: "" }; try { - const unEscapedCode = unEscapeScript(code); + const sanitizedScript = sanitizeScript(code); /* wrapCode - Wrapping code in a function, since all code/script get wrapped with a function during evaluation. - Some syntaxes won't be valid unless they're at the RHS of a statement. + Some syntax won't be valid unless they're at the RHS of a statement. Since we're assigning all code/script to RHS during evaluation, we do the same here. So that during ast parse, those errors are neglected. */ /* e.g. IIFE without braces function() { return 123; }() -> is invalid - let result = function() { return 123; }() -> is valid + let result = function() { return 123; }() -> is valid */ - const wrappedCode = wrapCode(unEscapedCode); + const wrappedCode = wrapCode(sanitizedScript); ast = getAST(wrappedCode); } catch (e) { if (e instanceof SyntaxError) { @@ -196,7 +196,7 @@ export const extractIdentifiersFromCode = (code: string): string[] => { isMemberExpressionNode(parent) && /* Member expressions that are "computed" (with [ ] search) and the ones that have optional chaining ( a.b?.c ) - will be considered top level node. + will be considered top level node. We will stop looking for further parents */ /* "computed" exception - isArrayAccessorNode Member expressions that are array accessors with static index - [9] diff --git a/app/client/src/workers/evaluate.test.ts b/app/client/src/workers/evaluate.test.ts index 5d3742482a..f06e47e951 100644 --- a/app/client/src/workers/evaluate.test.ts +++ b/app/client/src/workers/evaluate.test.ts @@ -72,7 +72,7 @@ describe("evaluate", () => { const result = wrongJS return result; } - closedFunction() + closedFunction.call(THIS_CONTEXT) `, severity: "error", originalBinding: "wrongJS", @@ -86,7 +86,7 @@ describe("evaluate", () => { const result = wrongJS return result; } - closedFunction() + closedFunction.call(THIS_CONTEXT) `, severity: "error", originalBinding: "wrongJS", @@ -106,7 +106,7 @@ describe("evaluate", () => { const result = {}.map() return result; } - closedFunction() + closedFunction.call(THIS_CONTEXT) `, severity: "error", originalBinding: "{}.map()", @@ -121,7 +121,7 @@ describe("evaluate", () => { }); it("gets triggers from a function", () => { const js = "showAlert('message', 'info')"; - const response = evaluate(js, dataTree, {}, undefined, true); + const response = evaluate(js, dataTree, {}, undefined, undefined, true); //this will be changed again in new implemenation for promises const data = { action: { @@ -172,7 +172,7 @@ describe("evaluate", () => { const result = setTimeout(() => {}, 100) return result; } - closedFunction() + closedFunction.call(THIS_CONTEXT) `, severity: "error", originalBinding: "setTimeout(() => {}, 100)", @@ -188,7 +188,23 @@ describe("evaluate", () => { it("evaluates functions with callback data", () => { const js = "(arg1, arg2) => arg1.value + arg2"; const callbackData = [{ value: "test" }, "1"]; - const response = evaluate(js, dataTree, {}, callbackData); + const response = evaluate(js, dataTree, {}, {}, callbackData); expect(response.result).toBe("test1"); }); + it("has access to this context", () => { + const js = "this.contextVariable"; + const thisContext = { contextVariable: "test" }; + const response = evaluate(js, dataTree, {}, { thisContext }); + expect(response.result).toBe("test"); + // there should not be any error when accessing "this" variables + expect(response.errors).toHaveLength(0); + }); + + it("has access to additional global context", () => { + const js = "contextVariable"; + const globalContext = { contextVariable: "test" }; + const response = evaluate(js, dataTree, {}, { globalContext }); + expect(response.result).toBe("test"); + expect(response.errors).toHaveLength(0); + }); }); diff --git a/app/client/src/workers/evaluate.ts b/app/client/src/workers/evaluate.ts index 294409c3c1..bfa5045535 100644 --- a/app/client/src/workers/evaluate.ts +++ b/app/client/src/workers/evaluate.ts @@ -32,12 +32,12 @@ export const EvaluationScripts: Record = { const result = ${ScriptTemplate} return result; } - closedFunction() + closedFunction.call(THIS_CONTEXT) `, [EvaluationScriptType.ANONYMOUS_FUNCTION]: ` function callback (script) { const userFunction = script; - const result = userFunction.apply(self, ARGUMENTS); + const result = userFunction.apply(THIS_CONTEXT, ARGUMENTS); return result; } callback(${ScriptTemplate}) @@ -47,7 +47,7 @@ export const EvaluationScripts: Record = { const result = ${ScriptTemplate} return result } - closedFunction(); + closedFunction.call(THIS_CONTEXT); `, }; @@ -95,11 +95,24 @@ export const createGlobalData = ( dataTree: DataTree, resolvedFunctions: Record, isTriggerBased: boolean, + context?: EvaluateContext, evalArguments?: Array, ) => { const GLOBAL_DATA: Record = {}; ///// Adding callback data GLOBAL_DATA.ARGUMENTS = evalArguments; + //// 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; + }); + } + } ///// Mocking Promise class GLOBAL_DATA.Promise = AppsmithPromise; if (isTriggerBased) { @@ -128,26 +141,34 @@ export const createGlobalData = ( return GLOBAL_DATA; }; -export function unEscapeScript(js: string) { +export function sanitizeScript(js: string) { // 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, ""); - const unescapedJS = - self.evaluationVersion > 1 ? trimmedJS : unescapeJS(trimmedJS); - return unescapedJS; + return self.evaluationVersion > 1 ? trimmedJS : unescapeJS(trimmedJS); } +/** Define a context just for this script + * thisContext will define it on the `this` + * globalContext will define it globally + */ +export type EvaluateContext = { + thisContext?: Record; + globalContext?: Record; +}; + export default function evaluate( js: string, data: DataTree, resolvedFunctions: Record, + context?: EvaluateContext, evalArguments?: Array, isTriggerBased = false, ): EvalResult { - const unescapedJS = unEscapeScript(js); + const sanitizedScript = sanitizeScript(js); const scriptType = getScriptType(evalArguments, isTriggerBased); - const script = getScriptToEval(unescapedJS, scriptType); + const script = getScriptToEval(sanitizedScript, scriptType); // We are linting original js binding, // This will make sure that the character count is not messed up when we do unescapejs const scriptToLint = getScriptToEval(js, scriptType); @@ -160,6 +181,7 @@ export default function evaluate( data, resolvedFunctions, isTriggerBased, + context, evalArguments, ); diff --git a/app/client/src/workers/evaluation.worker.ts b/app/client/src/workers/evaluation.worker.ts index 694fb6098f..bfbe50dd13 100644 --- a/app/client/src/workers/evaluation.worker.ts +++ b/app/client/src/workers/evaluation.worker.ts @@ -193,6 +193,7 @@ ctx.addEventListener( resolvedFunctions, EvaluationSubstitutionType.TEMPLATE, true, + undefined, callbackData, fullPropertyPath, ); @@ -265,6 +266,7 @@ ctx.addEventListener( evalTree, resolvedFunctions, undefined, + undefined, true, ); return result; @@ -274,7 +276,7 @@ ctx.addEventListener( const evalTree = dataTreeEvaluator?.evalTree; if (!evalTree) return {}; return isTrigger - ? evaluate(expression, evalTree, {}, [], true) + ? evaluate(expression, evalTree, {}, undefined, [], true) : evaluate(expression, evalTree, {}); case EVAL_WORKER_ACTIONS.UPDATE_REPLAY_OBJECT: const { entity, entityId, entityType } = requestData; diff --git a/app/client/src/workers/lint.ts b/app/client/src/workers/lint.ts index 33cfa6920c..8d07644913 100644 --- a/app/client/src/workers/lint.ts +++ b/app/client/src/workers/lint.ts @@ -6,7 +6,7 @@ import { } from "utils/DynamicBindingUtils"; import { JSHINT as jshint } from "jshint"; import { Severity } from "entities/AppsmithConsole"; -import { last, keys, isEmpty } from "lodash"; +import { isEmpty, keys, last } from "lodash"; import { EvaluationScripts, EvaluationScriptType, @@ -27,21 +27,20 @@ export const getPositionInEvaluationScript = ( return { line: lines.length, ch: lastLine.length }; }; -const EvalutionScriptPositions: Record = {}; +const EvaluationScriptPositions: Record = {}; function getEvaluationScriptPosition(scriptType: EvaluationScriptType) { - if (isEmpty(EvalutionScriptPositions)) { + if (isEmpty(EvaluationScriptPositions)) { // We are computing position of <