mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 01:56:46 +00:00
refactor(core): Remove legacy expression evaluator (#14518)
This commit is contained in:
@@ -43,7 +43,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@n8n/tournament": "1.0.6",
|
||||
"@n8n_io/riot-tmpl": "4.0.0",
|
||||
"ast-types": "0.15.2",
|
||||
"axios": "catalog:",
|
||||
"callsites": "catalog:",
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import * as tmpl from '@n8n_io/riot-tmpl';
|
||||
import { DateTime, Duration, Interval } from 'luxon';
|
||||
|
||||
import { ApplicationError } from './errors/application.error';
|
||||
@@ -53,7 +52,7 @@ export class Expression {
|
||||
constructor(private readonly workflow: Workflow) {}
|
||||
|
||||
static resolveWithoutWorkflow(expression: string, data: IDataObject = {}) {
|
||||
return tmpl.tmpl(expression, data);
|
||||
return evaluateExpression(expression, data);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -327,10 +326,7 @@ export class Expression {
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
private renderExpression(
|
||||
expression: string,
|
||||
data: IWorkflowDataProxyData,
|
||||
): tmpl.ReturnValue | undefined {
|
||||
private renderExpression(expression: string, data: IWorkflowDataProxyData) {
|
||||
try {
|
||||
return evaluateExpression(expression, data);
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,155 +1,21 @@
|
||||
import type { ReturnValue, TmplDifference } from '@n8n/tournament';
|
||||
import { Tournament } from '@n8n/tournament';
|
||||
import * as tmpl from '@n8n_io/riot-tmpl';
|
||||
|
||||
import { PrototypeSanitizer } from './ExpressionSandboxing';
|
||||
import type { ExpressionEvaluatorType } from './Interfaces';
|
||||
import * as LoggerProxy from './LoggerProxy';
|
||||
|
||||
type Evaluator = (expr: string, data: unknown) => tmpl.ReturnValue;
|
||||
type Evaluator = (expr: string, data: unknown) => string | null | (() => unknown);
|
||||
type ErrorHandler = (error: Error) => void;
|
||||
type DifferenceHandler = (expr: string) => void;
|
||||
|
||||
// Set it to use double curly brackets instead of single ones
|
||||
tmpl.brackets.set('{{ }}');
|
||||
|
||||
let errorHandler: ErrorHandler = () => {};
|
||||
let differenceHandler: DifferenceHandler = () => {};
|
||||
const differenceChecker = (diff: TmplDifference) => {
|
||||
try {
|
||||
if (diff.same) {
|
||||
return;
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||
if (diff.has?.function || diff.has?.templateString) {
|
||||
return;
|
||||
}
|
||||
if (diff.expression === 'UNPARSEABLE') {
|
||||
differenceHandler(diff.expression);
|
||||
} else {
|
||||
differenceHandler(diff.expression.value);
|
||||
}
|
||||
} catch {
|
||||
LoggerProxy.error('Expression evaluator difference checker failed');
|
||||
}
|
||||
};
|
||||
const errorHandler: ErrorHandler = () => {};
|
||||
const tournamentEvaluator = new Tournament(errorHandler, undefined, undefined, {
|
||||
before: [],
|
||||
after: [PrototypeSanitizer],
|
||||
});
|
||||
let evaluator: Evaluator = tmpl.tmpl;
|
||||
let currentEvaluatorType: ExpressionEvaluatorType = 'tmpl';
|
||||
let diffExpressions = false;
|
||||
const evaluator: Evaluator = tournamentEvaluator.execute.bind(tournamentEvaluator);
|
||||
|
||||
export const setErrorHandler = (handler: ErrorHandler) => {
|
||||
errorHandler = handler;
|
||||
tmpl.tmpl.errorHandler = handler;
|
||||
tournamentEvaluator.errorHandler = handler;
|
||||
};
|
||||
|
||||
export const setEvaluator = (evalType: ExpressionEvaluatorType) => {
|
||||
currentEvaluatorType = evalType;
|
||||
if (evalType === 'tmpl') {
|
||||
evaluator = tmpl.tmpl;
|
||||
} else if (evalType === 'tournament') {
|
||||
evaluator = tournamentEvaluator.execute.bind(tournamentEvaluator);
|
||||
}
|
||||
};
|
||||
|
||||
export const setDiffReporter = (reporter: (expr: string) => void) => {
|
||||
differenceHandler = reporter;
|
||||
};
|
||||
|
||||
export const setDifferEnabled = (enabled: boolean) => {
|
||||
diffExpressions = enabled;
|
||||
};
|
||||
|
||||
const diffCache: Record<string, TmplDifference | null> = {};
|
||||
|
||||
export const checkEvaluatorDifferences = (expr: string): TmplDifference | null => {
|
||||
if (expr in diffCache) {
|
||||
return diffCache[expr];
|
||||
}
|
||||
let diff: TmplDifference | null;
|
||||
try {
|
||||
diff = tournamentEvaluator.tmplDiff(expr);
|
||||
} catch {
|
||||
// We don't include the expression for privacy reasons
|
||||
try {
|
||||
differenceHandler('ERROR');
|
||||
} catch {}
|
||||
diff = null;
|
||||
}
|
||||
|
||||
if (diff?.same === false) {
|
||||
differenceChecker(diff);
|
||||
}
|
||||
|
||||
diffCache[expr] = diff;
|
||||
return diff;
|
||||
};
|
||||
|
||||
export const getEvaluator = () => {
|
||||
return evaluator;
|
||||
};
|
||||
|
||||
export const evaluateExpression: Evaluator = (expr, data) => {
|
||||
if (!diffExpressions) {
|
||||
return evaluator(expr, data);
|
||||
}
|
||||
const diff = checkEvaluatorDifferences(expr);
|
||||
|
||||
// We already know that they're different so don't bother
|
||||
// evaluating with both evaluators
|
||||
if (!diff?.same) {
|
||||
return evaluator(expr, data);
|
||||
}
|
||||
|
||||
let tmplValue: tmpl.ReturnValue;
|
||||
let tournValue: ReturnValue;
|
||||
let wasTmplError = false;
|
||||
let tmplError: unknown;
|
||||
let wasTournError = false;
|
||||
let tournError: unknown;
|
||||
|
||||
try {
|
||||
tmplValue = tmpl.tmpl(expr, data);
|
||||
} catch (error) {
|
||||
tmplError = error;
|
||||
wasTmplError = true;
|
||||
}
|
||||
|
||||
try {
|
||||
tournValue = tournamentEvaluator.execute(expr, data);
|
||||
} catch (error) {
|
||||
tournError = error;
|
||||
wasTournError = true;
|
||||
}
|
||||
|
||||
if (
|
||||
wasTmplError !== wasTournError ||
|
||||
JSON.stringify(tmplValue!) !== JSON.stringify(tournValue!)
|
||||
) {
|
||||
try {
|
||||
if (diff.expression) {
|
||||
differenceHandler(diff.expression.value);
|
||||
} else {
|
||||
differenceHandler('VALUEDIFF');
|
||||
}
|
||||
} catch {
|
||||
LoggerProxy.error('Failed to report error difference');
|
||||
}
|
||||
}
|
||||
|
||||
if (currentEvaluatorType === 'tmpl') {
|
||||
if (wasTmplError) {
|
||||
throw tmplError;
|
||||
}
|
||||
return tmplValue!;
|
||||
}
|
||||
|
||||
if (wasTournError) {
|
||||
throw tournError;
|
||||
}
|
||||
return tournValue!;
|
||||
return evaluator(expr, data);
|
||||
};
|
||||
|
||||
@@ -134,7 +134,7 @@ export const extendTransform = (expression: string): { code: string } | undefine
|
||||
const chainNumber = currentChain;
|
||||
currentChain += 1;
|
||||
|
||||
// This is to match our fork of tmpl
|
||||
// This is to match behavior in our original expression evaluator (tmpl)
|
||||
const globalIdentifier = types.builders.identifier(
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
typeof window !== 'object' ? 'global' : 'window',
|
||||
|
||||
@@ -6,8 +6,9 @@ export interface ExpressionText {
|
||||
export interface ExpressionCode {
|
||||
type: 'code';
|
||||
text: string;
|
||||
// tmpl has different behaviours if the last expression
|
||||
// doesn't close itself.
|
||||
|
||||
// This is to match behavior in our original expression evaluator (tmpl),
|
||||
// which has different behaviours if the last expression doesn't close itself.
|
||||
hasClosingBrackets: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -2872,8 +2872,6 @@ export interface ICheckProcessedContextData {
|
||||
};
|
||||
}
|
||||
|
||||
export type ExpressionEvaluatorType = 'tmpl' | 'tournament';
|
||||
|
||||
export type N8nAIProviderType = 'openai' | 'unknown';
|
||||
|
||||
export interface SecretsHelpersBase {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import * as LoggerProxy from './LoggerProxy';
|
||||
export * as ExpressionEvaluatorProxy from './ExpressionEvaluatorProxy';
|
||||
import * as NodeHelpers from './NodeHelpers';
|
||||
import * as ObservableObject from './ObservableObject';
|
||||
import * as TelemetryHelpers from './TelemetryHelpers';
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
import { DateTime, Duration, Interval } from 'luxon';
|
||||
|
||||
import { ExpressionError } from '@/errors/expression.error';
|
||||
import { setDifferEnabled, setEvaluator } from '@/ExpressionEvaluatorProxy';
|
||||
import { extendSyntax } from '@/Extensions/ExpressionExtension';
|
||||
import type { INodeExecutionData } from '@/Interfaces';
|
||||
import { Workflow } from '@/Workflow';
|
||||
@@ -15,202 +14,206 @@ import { baseFixtures } from './ExpressionFixtures/base';
|
||||
import type { ExpressionTestEvaluation, ExpressionTestTransform } from './ExpressionFixtures/base';
|
||||
import * as Helpers from './Helpers';
|
||||
|
||||
setDifferEnabled(true);
|
||||
describe('Expression', () => {
|
||||
describe('getParameterValue()', () => {
|
||||
const nodeTypes = Helpers.NodeTypes();
|
||||
const workflow = new Workflow({
|
||||
id: '1',
|
||||
nodes: [
|
||||
{
|
||||
name: 'node',
|
||||
typeVersion: 1,
|
||||
type: 'test.set',
|
||||
id: 'uuid-1234',
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
},
|
||||
],
|
||||
connections: {},
|
||||
active: false,
|
||||
nodeTypes,
|
||||
});
|
||||
const expression = workflow.expression;
|
||||
|
||||
for (const evaluator of ['tmpl', 'tournament'] as const) {
|
||||
setEvaluator(evaluator);
|
||||
describe(`Expression (with ${evaluator})`, () => {
|
||||
describe('getParameterValue()', () => {
|
||||
const nodeTypes = Helpers.NodeTypes();
|
||||
const workflow = new Workflow({
|
||||
id: '1',
|
||||
nodes: [
|
||||
{
|
||||
name: 'node',
|
||||
typeVersion: 1,
|
||||
type: 'test.set',
|
||||
id: 'uuid-1234',
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
},
|
||||
],
|
||||
connections: {},
|
||||
active: false,
|
||||
nodeTypes,
|
||||
});
|
||||
const expression = workflow.expression;
|
||||
const evaluate = (value: string) =>
|
||||
expression.getParameterValue(value, null, 0, 0, 'node', [], 'manual', {});
|
||||
|
||||
const evaluate = (value: string) =>
|
||||
expression.getParameterValue(value, null, 0, 0, 'node', [], 'manual', {});
|
||||
it('should not be able to use global built-ins from denylist', () => {
|
||||
expect(evaluate('={{document}}')).toEqual({});
|
||||
expect(evaluate('={{window}}')).toEqual({});
|
||||
|
||||
it('should not be able to use global built-ins from denylist', () => {
|
||||
expect(evaluate('={{document}}')).toEqual({});
|
||||
expect(evaluate('={{window}}')).toEqual({});
|
||||
expect(evaluate('={{Window}}')).toEqual({});
|
||||
expect(evaluate('={{globalThis}}')).toEqual({});
|
||||
expect(evaluate('={{self}}')).toEqual({});
|
||||
|
||||
expect(evaluate('={{Window}}')).toEqual({});
|
||||
expect(evaluate('={{globalThis}}')).toEqual({});
|
||||
expect(evaluate('={{self}}')).toEqual({});
|
||||
expect(evaluate('={{alert}}')).toEqual({});
|
||||
expect(evaluate('={{prompt}}')).toEqual({});
|
||||
expect(evaluate('={{confirm}}')).toEqual({});
|
||||
|
||||
expect(evaluate('={{alert}}')).toEqual({});
|
||||
expect(evaluate('={{prompt}}')).toEqual({});
|
||||
expect(evaluate('={{confirm}}')).toEqual({});
|
||||
expect(evaluate('={{eval}}')).toEqual({});
|
||||
expect(evaluate('={{uneval}}')).toEqual({});
|
||||
expect(evaluate('={{setTimeout}}')).toEqual({});
|
||||
expect(evaluate('={{setInterval}}')).toEqual({});
|
||||
expect(evaluate('={{Function}}')).toEqual({});
|
||||
|
||||
expect(evaluate('={{eval}}')).toEqual({});
|
||||
expect(evaluate('={{uneval}}')).toEqual({});
|
||||
expect(evaluate('={{setTimeout}}')).toEqual({});
|
||||
expect(evaluate('={{setInterval}}')).toEqual({});
|
||||
expect(evaluate('={{Function}}')).toEqual({});
|
||||
expect(evaluate('={{fetch}}')).toEqual({});
|
||||
expect(evaluate('={{XMLHttpRequest}}')).toEqual({});
|
||||
|
||||
expect(evaluate('={{fetch}}')).toEqual({});
|
||||
expect(evaluate('={{XMLHttpRequest}}')).toEqual({});
|
||||
expect(evaluate('={{Promise}}')).toEqual({});
|
||||
expect(evaluate('={{Generator}}')).toEqual({});
|
||||
expect(evaluate('={{GeneratorFunction}}')).toEqual({});
|
||||
expect(evaluate('={{AsyncFunction}}')).toEqual({});
|
||||
expect(evaluate('={{AsyncGenerator}}')).toEqual({});
|
||||
expect(evaluate('={{AsyncGeneratorFunction}}')).toEqual({});
|
||||
|
||||
expect(evaluate('={{Promise}}')).toEqual({});
|
||||
expect(evaluate('={{Generator}}')).toEqual({});
|
||||
expect(evaluate('={{GeneratorFunction}}')).toEqual({});
|
||||
expect(evaluate('={{AsyncFunction}}')).toEqual({});
|
||||
expect(evaluate('={{AsyncGenerator}}')).toEqual({});
|
||||
expect(evaluate('={{AsyncGeneratorFunction}}')).toEqual({});
|
||||
expect(evaluate('={{WebAssembly}}')).toEqual({});
|
||||
|
||||
expect(evaluate('={{WebAssembly}}')).toEqual({});
|
||||
expect(evaluate('={{Reflect}}')).toEqual({});
|
||||
expect(evaluate('={{Proxy}}')).toEqual({});
|
||||
|
||||
expect(evaluate('={{Reflect}}')).toEqual({});
|
||||
expect(evaluate('={{Proxy}}')).toEqual({});
|
||||
expect(() => evaluate('={{constructor}}')).toThrowError(
|
||||
new ExpressionError('Cannot access "constructor" due to security concerns'),
|
||||
);
|
||||
|
||||
expect(() => evaluate('={{constructor}}')).toThrowError(
|
||||
new ExpressionError('Cannot access "constructor" due to security concerns'),
|
||||
);
|
||||
|
||||
expect(evaluate('={{escape}}')).toEqual({});
|
||||
expect(evaluate('={{unescape}}')).toEqual({});
|
||||
});
|
||||
|
||||
it('should be able to use global built-ins from allowlist', () => {
|
||||
expect(evaluate('={{new Date()}}')).toBeInstanceOf(Date);
|
||||
expect(evaluate('={{DateTime.now().toLocaleString()}}')).toEqual(
|
||||
DateTime.now().toLocaleString(),
|
||||
);
|
||||
|
||||
jest.useFakeTimers({ now: new Date() });
|
||||
expect(evaluate('={{Interval.after(new Date(), 100)}}')).toEqual(
|
||||
Interval.after(new Date(), 100),
|
||||
);
|
||||
jest.useRealTimers();
|
||||
|
||||
expect(evaluate('={{Duration.fromMillis(100)}}')).toEqual(Duration.fromMillis(100));
|
||||
|
||||
expect(evaluate('={{new Object()}}')).toEqual(new Object());
|
||||
|
||||
expect(evaluate('={{new Array()}}')).toEqual([]);
|
||||
expect(evaluate('={{new Int8Array()}}')).toEqual(new Int8Array());
|
||||
expect(evaluate('={{new Uint8Array()}}')).toEqual(new Uint8Array());
|
||||
expect(evaluate('={{new Uint8ClampedArray()}}')).toEqual(new Uint8ClampedArray());
|
||||
expect(evaluate('={{new Int16Array()}}')).toEqual(new Int16Array());
|
||||
expect(evaluate('={{new Uint16Array()}}')).toEqual(new Uint16Array());
|
||||
expect(evaluate('={{new Int32Array()}}')).toEqual(new Int32Array());
|
||||
expect(evaluate('={{new Uint32Array()}}')).toEqual(new Uint32Array());
|
||||
expect(evaluate('={{new Float32Array()}}')).toEqual(new Float32Array());
|
||||
expect(evaluate('={{new Float64Array()}}')).toEqual(new Float64Array());
|
||||
expect(evaluate('={{new BigInt64Array()}}')).toEqual(new BigInt64Array());
|
||||
expect(evaluate('={{new BigUint64Array()}}')).toEqual(new BigUint64Array());
|
||||
|
||||
expect(evaluate('={{new Map()}}')).toEqual(new Map());
|
||||
expect(evaluate('={{new WeakMap()}}')).toEqual(new WeakMap());
|
||||
expect(evaluate('={{new Set()}}')).toEqual(new Set());
|
||||
expect(evaluate('={{new WeakSet()}}')).toEqual(new WeakSet());
|
||||
|
||||
expect(evaluate('={{new Error()}}')).toEqual(new Error());
|
||||
expect(evaluate('={{new TypeError()}}')).toEqual(new TypeError());
|
||||
expect(evaluate('={{new SyntaxError()}}')).toEqual(new SyntaxError());
|
||||
expect(evaluate('={{new EvalError()}}')).toEqual(new EvalError());
|
||||
expect(evaluate('={{new RangeError()}}')).toEqual(new RangeError());
|
||||
expect(evaluate('={{new ReferenceError()}}')).toEqual(new ReferenceError());
|
||||
expect(evaluate('={{new URIError()}}')).toEqual(new URIError());
|
||||
|
||||
expect(evaluate('={{Intl}}')).toEqual(Intl);
|
||||
|
||||
expect(evaluate('={{new String()}}')).toEqual(new String());
|
||||
expect(evaluate("={{new RegExp('')}}")).toEqual(new RegExp(''));
|
||||
|
||||
expect(evaluate('={{Math}}')).toEqual(Math);
|
||||
expect(evaluate('={{new Number()}}')).toEqual(new Number());
|
||||
expect(evaluate("={{BigInt('1')}}")).toEqual(BigInt('1'));
|
||||
expect(evaluate('={{Infinity}}')).toEqual(Infinity);
|
||||
expect(evaluate('={{NaN}}')).toEqual(NaN);
|
||||
expect(evaluate('={{isFinite(1)}}')).toEqual(isFinite(1));
|
||||
expect(evaluate('={{isNaN(1)}}')).toEqual(isNaN(1));
|
||||
expect(evaluate("={{parseFloat('1')}}")).toEqual(parseFloat('1'));
|
||||
expect(evaluate("={{parseInt('1', 10)}}")).toEqual(parseInt('1', 10));
|
||||
|
||||
expect(evaluate('={{JSON.stringify({})}}')).toEqual(JSON.stringify({}));
|
||||
expect(evaluate('={{new ArrayBuffer(10)}}')).toEqual(new ArrayBuffer(10));
|
||||
expect(evaluate('={{new SharedArrayBuffer(10)}}')).toEqual(new SharedArrayBuffer(10));
|
||||
expect(evaluate('={{Atomics}}')).toEqual(Atomics);
|
||||
expect(evaluate('={{new DataView(new ArrayBuffer(1))}}')).toEqual(
|
||||
new DataView(new ArrayBuffer(1)),
|
||||
);
|
||||
|
||||
expect(evaluate("={{encodeURI('https://google.com')}}")).toEqual(
|
||||
encodeURI('https://google.com'),
|
||||
);
|
||||
expect(evaluate("={{encodeURIComponent('https://google.com')}}")).toEqual(
|
||||
encodeURIComponent('https://google.com'),
|
||||
);
|
||||
expect(evaluate("={{decodeURI('https://google.com')}}")).toEqual(
|
||||
decodeURI('https://google.com'),
|
||||
);
|
||||
expect(evaluate("={{decodeURIComponent('https://google.com')}}")).toEqual(
|
||||
decodeURIComponent('https://google.com'),
|
||||
);
|
||||
|
||||
expect(evaluate('={{Boolean(1)}}')).toEqual(Boolean(1));
|
||||
expect(evaluate('={{Symbol(1).toString()}}')).toEqual(Symbol(1).toString());
|
||||
});
|
||||
expect(evaluate('={{escape}}')).toEqual({});
|
||||
expect(evaluate('={{unescape}}')).toEqual({});
|
||||
});
|
||||
|
||||
describe('Test all expression value fixtures', () => {
|
||||
const expression = workflow.expression;
|
||||
it('should be able to use global built-ins from allowlist', () => {
|
||||
expect(evaluate('={{new Date()}}')).toBeInstanceOf(Date);
|
||||
expect(evaluate('={{DateTime.now().toLocaleString()}}')).toEqual(
|
||||
DateTime.now().toLocaleString(),
|
||||
);
|
||||
|
||||
const evaluate = (value: string, data: INodeExecutionData[]) => {
|
||||
const itemIndex = data.length === 0 ? -1 : 0;
|
||||
return expression.getParameterValue(value, null, 0, itemIndex, 'node', data, 'manual', {});
|
||||
};
|
||||
jest.useFakeTimers({ now: new Date() });
|
||||
expect(evaluate('={{Interval.after(new Date(), 100)}}')).toEqual(
|
||||
Interval.after(new Date(), 100),
|
||||
);
|
||||
jest.useRealTimers();
|
||||
|
||||
for (const t of baseFixtures) {
|
||||
if (!t.tests.some((test) => test.type === 'evaluation')) {
|
||||
continue;
|
||||
}
|
||||
test(t.expression, () => {
|
||||
const evaluationTests = t.tests.filter(
|
||||
(test): test is ExpressionTestEvaluation => test.type === 'evaluation',
|
||||
);
|
||||
expect(evaluate('={{Duration.fromMillis(100)}}')).toEqual(Duration.fromMillis(100));
|
||||
|
||||
for (const test of evaluationTests) {
|
||||
const input = test.input.map((d) => ({ json: d })) as any;
|
||||
expect(evaluate('={{new Object()}}')).toEqual(new Object());
|
||||
|
||||
if ('error' in test) {
|
||||
expect(() => evaluate(t.expression, input)).toThrowError(test.error);
|
||||
} else {
|
||||
expect(evaluate(t.expression, input)).toStrictEqual(test.output);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
expect(evaluate('={{new Array()}}')).toEqual([]);
|
||||
expect(evaluate('={{new Int8Array()}}')).toEqual(new Int8Array());
|
||||
expect(evaluate('={{new Uint8Array()}}')).toEqual(new Uint8Array());
|
||||
expect(evaluate('={{new Uint8ClampedArray()}}')).toEqual(new Uint8ClampedArray());
|
||||
expect(evaluate('={{new Int16Array()}}')).toEqual(new Int16Array());
|
||||
expect(evaluate('={{new Uint16Array()}}')).toEqual(new Uint16Array());
|
||||
expect(evaluate('={{new Int32Array()}}')).toEqual(new Int32Array());
|
||||
expect(evaluate('={{new Uint32Array()}}')).toEqual(new Uint32Array());
|
||||
expect(evaluate('={{new Float32Array()}}')).toEqual(new Float32Array());
|
||||
expect(evaluate('={{new Float64Array()}}')).toEqual(new Float64Array());
|
||||
expect(evaluate('={{new BigInt64Array()}}')).toEqual(new BigInt64Array());
|
||||
expect(evaluate('={{new BigUint64Array()}}')).toEqual(new BigUint64Array());
|
||||
|
||||
expect(evaluate('={{new Map()}}')).toEqual(new Map());
|
||||
expect(evaluate('={{new WeakMap()}}')).toEqual(new WeakMap());
|
||||
expect(evaluate('={{new Set()}}')).toEqual(new Set());
|
||||
expect(evaluate('={{new WeakSet()}}')).toEqual(new WeakSet());
|
||||
|
||||
expect(evaluate('={{new Error()}}')).toEqual(new Error());
|
||||
expect(evaluate('={{new TypeError()}}')).toEqual(new TypeError());
|
||||
expect(evaluate('={{new SyntaxError()}}')).toEqual(new SyntaxError());
|
||||
expect(evaluate('={{new EvalError()}}')).toEqual(new EvalError());
|
||||
expect(evaluate('={{new RangeError()}}')).toEqual(new RangeError());
|
||||
expect(evaluate('={{new ReferenceError()}}')).toEqual(new ReferenceError());
|
||||
expect(evaluate('={{new URIError()}}')).toEqual(new URIError());
|
||||
|
||||
expect(evaluate('={{Intl}}')).toEqual(Intl);
|
||||
|
||||
expect(evaluate('={{new String()}}')).toEqual(new String());
|
||||
expect(evaluate("={{new RegExp('')}}")).toEqual(new RegExp(''));
|
||||
|
||||
expect(evaluate('={{Math}}')).toEqual(Math);
|
||||
expect(evaluate('={{new Number()}}')).toEqual(new Number());
|
||||
expect(evaluate("={{BigInt('1')}}")).toEqual(BigInt('1'));
|
||||
expect(evaluate('={{Infinity}}')).toEqual(Infinity);
|
||||
expect(evaluate('={{NaN}}')).toEqual(NaN);
|
||||
expect(evaluate('={{isFinite(1)}}')).toEqual(isFinite(1));
|
||||
expect(evaluate('={{isNaN(1)}}')).toEqual(isNaN(1));
|
||||
expect(evaluate("={{parseFloat('1')}}")).toEqual(parseFloat('1'));
|
||||
expect(evaluate("={{parseInt('1', 10)}}")).toEqual(parseInt('1', 10));
|
||||
|
||||
expect(evaluate('={{JSON.stringify({})}}')).toEqual(JSON.stringify({}));
|
||||
expect(evaluate('={{new ArrayBuffer(10)}}')).toEqual(new ArrayBuffer(10));
|
||||
expect(evaluate('={{new SharedArrayBuffer(10)}}')).toEqual(new SharedArrayBuffer(10));
|
||||
expect(evaluate('={{Atomics}}')).toEqual(Atomics);
|
||||
expect(evaluate('={{new DataView(new ArrayBuffer(1))}}')).toEqual(
|
||||
new DataView(new ArrayBuffer(1)),
|
||||
);
|
||||
|
||||
expect(evaluate("={{encodeURI('https://google.com')}}")).toEqual(
|
||||
encodeURI('https://google.com'),
|
||||
);
|
||||
expect(evaluate("={{encodeURIComponent('https://google.com')}}")).toEqual(
|
||||
encodeURIComponent('https://google.com'),
|
||||
);
|
||||
expect(evaluate("={{decodeURI('https://google.com')}}")).toEqual(
|
||||
decodeURI('https://google.com'),
|
||||
);
|
||||
expect(evaluate("={{decodeURIComponent('https://google.com')}}")).toEqual(
|
||||
decodeURIComponent('https://google.com'),
|
||||
);
|
||||
|
||||
expect(evaluate('={{Boolean(1)}}')).toEqual(Boolean(1));
|
||||
expect(evaluate('={{Symbol(1).toString()}}')).toEqual(Symbol(1).toString());
|
||||
});
|
||||
|
||||
describe('Test all expression transform fixtures', () => {
|
||||
for (const t of baseFixtures) {
|
||||
if (!t.tests.some((test) => test.type === 'transform')) {
|
||||
continue;
|
||||
}
|
||||
test(t.expression, () => {
|
||||
for (const test of t.tests.filter(
|
||||
(test): test is ExpressionTestTransform => test.type === 'transform',
|
||||
)) {
|
||||
const expr = t.expression;
|
||||
expect(extendSyntax(expr, test.forceTransform)).toEqual(test.result ?? expr);
|
||||
}
|
||||
});
|
||||
}
|
||||
it('should not able to do arbitrary code execution', () => {
|
||||
const testFn = jest.fn();
|
||||
Object.assign(global, { testFn });
|
||||
expect(() => evaluate("={{ Date['constructor']('testFn()')()}}")).toThrowError(
|
||||
new ExpressionError('Cannot access "constructor" due to security concerns'),
|
||||
);
|
||||
expect(testFn).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
describe('Test all expression value fixtures', () => {
|
||||
const expression = workflow.expression;
|
||||
|
||||
const evaluate = (value: string, data: INodeExecutionData[]) => {
|
||||
const itemIndex = data.length === 0 ? -1 : 0;
|
||||
return expression.getParameterValue(value, null, 0, itemIndex, 'node', data, 'manual', {});
|
||||
};
|
||||
|
||||
for (const t of baseFixtures) {
|
||||
if (!t.tests.some((test) => test.type === 'evaluation')) {
|
||||
continue;
|
||||
}
|
||||
test(t.expression, () => {
|
||||
const evaluationTests = t.tests.filter(
|
||||
(test): test is ExpressionTestEvaluation => test.type === 'evaluation',
|
||||
);
|
||||
|
||||
for (const test of evaluationTests) {
|
||||
const input = test.input.map((d) => ({ json: d })) as any;
|
||||
|
||||
if ('error' in test) {
|
||||
expect(() => evaluate(t.expression, input)).toThrowError(test.error);
|
||||
} else {
|
||||
expect(evaluate(t.expression, input)).toStrictEqual(test.output);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('Test all expression transform fixtures', () => {
|
||||
for (const t of baseFixtures) {
|
||||
if (!t.tests.some((test) => test.type === 'transform')) {
|
||||
continue;
|
||||
}
|
||||
test(t.expression, () => {
|
||||
for (const test of t.tests.filter(
|
||||
(test): test is ExpressionTestTransform => test.type === 'transform',
|
||||
)) {
|
||||
const expr = t.expression;
|
||||
expect(extendSyntax(expr, test.forceTransform)).toEqual(test.result ?? expr);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -30,7 +30,7 @@ describe('Expression Extension Transforms', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('tmpl Expression Parser', () => {
|
||||
describe('Expression Parser', () => {
|
||||
describe('Compatible splitting', () => {
|
||||
test('Lone expression', () => {
|
||||
expect(splitExpression('{{ "" }}')).toEqual([
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ApplicationError } from '@/errors';
|
||||
import { objectExtensions } from '@/Extensions/ObjectExtensions';
|
||||
|
||||
import { evaluate } from './Helpers';
|
||||
@@ -126,7 +127,9 @@ describe('Data Transformation Functions', () => {
|
||||
test('should not allow prototype pollution', () => {
|
||||
['{__proto__: {polluted: true}}', '{constructor: {prototype: {polluted: true}}}'].forEach(
|
||||
(testExpression) => {
|
||||
expect(evaluate(`={{ (${testExpression}).compact() }}`)).toEqual(null);
|
||||
expect(() => evaluate(`={{ (${testExpression}).compact() }}`)).toThrow(
|
||||
ApplicationError,
|
||||
);
|
||||
expect(({} as any).polluted).toBeUndefined();
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user