From 890c2bd52bd2ccd94fbc2e626dceda30554f9e82 Mon Sep 17 00:00:00 2001 From: Elias Meire Date: Wed, 21 Feb 2024 10:43:34 +0100 Subject: [PATCH] fix(editor): Escape node names with quotes in autocomplete and drag'n'drop (#8663) --- .../completions/base.completions.ts | 5 +++-- .../src/components/VariableSelector.vue | 18 ++++++++++++++---- .../completions/dollar.completions.ts | 3 ++- .../src/utils/__tests__/mappingUtils.test.ts | 10 +++++++++- packages/editor-ui/src/utils/mappingUtils.ts | 8 +++++++- 5 files changed, 35 insertions(+), 9 deletions(-) diff --git a/packages/editor-ui/src/components/CodeNodeEditor/completions/base.completions.ts b/packages/editor-ui/src/components/CodeNodeEditor/completions/base.completions.ts index 9ed0ddea8e217..f03470915b729 100644 --- a/packages/editor-ui/src/components/CodeNodeEditor/completions/base.completions.ts +++ b/packages/editor-ui/src/components/CodeNodeEditor/completions/base.completions.ts @@ -5,6 +5,7 @@ import type { Completion, CompletionContext, CompletionResult } from '@codemirro import type { INodeUi } from '@/Interface'; import { mapStores } from 'pinia'; import { useWorkflowsStore } from '@/stores/workflows.store'; +import { escapeMappingString } from '@/utils/mappingUtils'; function getAutoCompletableNodeNames(nodes: INodeUi[]) { return nodes @@ -98,7 +99,7 @@ export const baseCompletions = defineComponent({ options.push( ...getAutoCompletableNodeNames(this.workflowsStore.allNodes).map((nodeName) => { return { - label: `${prefix}('${nodeName}')`, + label: `${prefix}('${escapeMappingString(nodeName)}')`, type: 'variable', info: this.$locale.baseText('codeNodeEditor.completer.$()', { interpolate: { nodeName }, @@ -138,7 +139,7 @@ export const baseCompletions = defineComponent({ const options: Completion[] = getAutoCompletableNodeNames(this.workflowsStore.allNodes).map( (nodeName) => { return { - label: `${prefix}('${nodeName}')`, + label: `${prefix}('${escapeMappingString(nodeName)}')`, type: 'variable', info: this.$locale.baseText('codeNodeEditor.completer.$()', { interpolate: { nodeName }, diff --git a/packages/editor-ui/src/components/VariableSelector.vue b/packages/editor-ui/src/components/VariableSelector.vue index 30b2f49d9239d..aa18ea1d27e71 100644 --- a/packages/editor-ui/src/components/VariableSelector.vue +++ b/packages/editor-ui/src/components/VariableSelector.vue @@ -50,6 +50,7 @@ import { useRootStore } from '@/stores/n8nRoot.store'; import { useNDVStore } from '@/stores/ndv.store'; import { useRouter } from 'vue-router'; import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers'; +import { escapeMappingString } from '@/utils/mappingUtils'; // Node types that should not be displayed in variable selector const SKIPPED_NODE_TYPES = [STICKY_NODE_TYPE]; @@ -398,7 +399,9 @@ export default defineComponent({ // Get json data if (outputData.hasOwnProperty('json')) { - const jsonPropertyPrefix = useShort ? '$json' : `$('${nodeName}').item.json`; + const jsonPropertyPrefix = useShort + ? '$json' + : `$('${escapeMappingString(nodeName)}').item.json`; const jsonDataOptions: IVariableSelectorOption[] = []; for (const propertyName of Object.keys(outputData.json)) { @@ -423,7 +426,9 @@ export default defineComponent({ // Get binary data if (outputData.hasOwnProperty('binary')) { - const binaryPropertyPrefix = useShort ? '$binary' : `$('${nodeName}').item.binary`; + const binaryPropertyPrefix = useShort + ? '$binary' + : `$('${escapeMappingString(nodeName)}').item.binary`; const binaryData = []; let binaryPropertyData = []; @@ -537,7 +542,7 @@ export default defineComponent({ returnData.push({ name: key, - key: `$('${nodeName}').context["${key}"]`, + key: `$('${escapeMappingString(nodeName)}').context['${escapeMappingString(key)}']`, // @ts-ignore value: nodeContext[key], }); @@ -793,7 +798,12 @@ export default defineComponent({ { name: this.$locale.baseText('variableSelector.parameters'), options: this.sortOptions( - this.getNodeParameters(nodeName, `$('${nodeName}').params`, undefined, filterText), + this.getNodeParameters( + nodeName, + `$('${escapeMappingString(nodeName)}').params`, + undefined, + filterText, + ), ), } as IVariableSelectorOption, ]; diff --git a/packages/editor-ui/src/plugins/codemirror/completions/dollar.completions.ts b/packages/editor-ui/src/plugins/codemirror/completions/dollar.completions.ts index 4f5c101d2f8d0..0eedd1cf7baac 100644 --- a/packages/editor-ui/src/plugins/codemirror/completions/dollar.completions.ts +++ b/packages/editor-ui/src/plugins/codemirror/completions/dollar.completions.ts @@ -11,6 +11,7 @@ import { } from './utils'; import type { Completion, CompletionContext, CompletionResult } from '@codemirror/autocomplete'; import { useExternalSecretsStore } from '@/stores/externalSecrets.ee.store'; +import { escapeMappingString } from '@/utils/mappingUtils'; /** * Completions offered at the dollar position: `$|` @@ -90,7 +91,7 @@ export function dollarOptions() { }) .concat( autocompletableNodeNames().map((nodeName) => ({ - label: `$('${nodeName}')`, + label: `$('${escapeMappingString(nodeName)}')`, type: 'keyword', info: i18n.baseText('codeNodeEditor.completer.$()', { interpolate: { nodeName } }), })), diff --git a/packages/editor-ui/src/utils/__tests__/mappingUtils.test.ts b/packages/editor-ui/src/utils/__tests__/mappingUtils.test.ts index 97302edd3be0a..224b00be560be 100644 --- a/packages/editor-ui/src/utils/__tests__/mappingUtils.test.ts +++ b/packages/editor-ui/src/utils/__tests__/mappingUtils.test.ts @@ -1,5 +1,5 @@ import type { INodeProperties } from 'n8n-workflow'; -import { getMappedResult, getMappedExpression } from '../mappingUtils'; +import { getMappedResult, getMappedExpression, escapeMappingString } from '../mappingUtils'; const RLC_PARAM: INodeProperties = { displayName: 'Base', @@ -273,4 +273,12 @@ describe('Mapping Utils', () => { ); }); }); + describe('escapeMappingString', () => { + test.each([ + { input: 'Normal node name (here)', output: 'Normal node name (here)' }, + { input: "'Should es'ape quotes here'", output: "\\'Should es\\'ape quotes here\\'" }, + ])('should escape "$input" to "$output"', ({ input, output }) => { + expect(escapeMappingString(input)).toEqual(output); + }); + }); }); diff --git a/packages/editor-ui/src/utils/mappingUtils.ts b/packages/editor-ui/src/utils/mappingUtils.ts index 2591c3bf33702..35e524c88890d 100644 --- a/packages/editor-ui/src/utils/mappingUtils.ts +++ b/packages/editor-ui/src/utils/mappingUtils.ts @@ -18,6 +18,10 @@ export function generatePath(root: string, path: Array): string }, root); } +export function escapeMappingString(str: string): string { + return str.replace(/\'/g, "\\'"); +} + export function getMappedExpression({ nodeName, distanceFromActive, @@ -28,7 +32,9 @@ export function getMappedExpression({ path: Array | string; }) { const root = - distanceFromActive === 1 ? '$json' : generatePath(`$('${nodeName}')`, ['item', 'json']); + distanceFromActive === 1 + ? '$json' + : generatePath(`$('${escapeMappingString(nodeName)}')`, ['item', 'json']); if (typeof path === 'string') { return `{{ ${root}${path} }}`;