fix(n8n Form Node): Resolve expressions in HTML fields (#13755)

This commit is contained in:
Michael Kret
2025-03-11 05:40:58 +02:00
committed by GitHub
parent 4fe249580a
commit de23ae5558
5 changed files with 80 additions and 32 deletions

View File

@@ -336,15 +336,7 @@ export class Form extends Node {
}); });
} }
} else { } else {
fields = (context.getNodeParameter('formFields.values', []) as FormFieldsParameter).map( fields = context.getNodeParameter('formFields.values', []) as FormFieldsParameter;
(field) => {
if (field.fieldType === 'hiddenField') {
field.fieldLabel = field.fieldName as string;
}
return field;
},
);
} }
const method = context.getRequestObject().method; const method = context.getRequestObject().method;

View File

@@ -6,7 +6,7 @@ import {
type IWebhookResponseData, type IWebhookResponseData,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { renderForm, sanitizeHtml } from './utils'; import { renderForm } from './utils';
export const renderFormNode = async ( export const renderFormNode = async (
context: IWebhookFunctions, context: IWebhookFunctions,
@@ -42,12 +42,6 @@ export const renderFormNode = async (
) as string) || 'Submit'; ) as string) || 'Submit';
} }
for (const field of fields) {
if (field.fieldType === 'html') {
field.html = sanitizeHtml(field.html as string);
}
}
const appendAttribution = context.evaluateExpression( const appendAttribution = context.evaluateExpression(
`{{ $('${trigger?.name}').params.options?.appendAttribution === false ? false : true }}`, `{{ $('${trigger?.name}').params.options?.appendAttribution === false ? false : true }}`,
) as boolean; ) as boolean;

View File

@@ -1,4 +1,5 @@
import { type Response } from 'express'; import { type Response } from 'express';
import type { MockProxy } from 'jest-mock-extended';
import { mock } from 'jest-mock-extended'; import { mock } from 'jest-mock-extended';
import { import {
type FormFieldsParameter, type FormFieldsParameter,
@@ -9,10 +10,19 @@ import {
import { renderFormNode } from '../formNodeUtils'; import { renderFormNode } from '../formNodeUtils';
describe('formNodeUtils', () => { describe('formNodeUtils', () => {
let webhookFunctions: MockProxy<IWebhookFunctions>;
beforeEach(() => {
webhookFunctions = mock<IWebhookFunctions>();
});
afterEach(() => {
jest.clearAllMocks();
});
it('should sanitize custom html', async () => { it('should sanitize custom html', async () => {
const executeFunctions = mock<IWebhookFunctions>(); webhookFunctions.getNode.mockReturnValue({ typeVersion: 2.1 } as any);
executeFunctions.getNode.mockReturnValue({ typeVersion: 2.1 } as any); webhookFunctions.getNodeParameter.calledWith('options').mockReturnValue({
executeFunctions.getNodeParameter.calledWith('options').mockReturnValue({
formTitle: 'Test Title', formTitle: 'Test Title',
formDescription: 'Test Description', formDescription: 'Test Description',
buttonLabel: 'Test Button Label', buttonLabel: 'Test Button Label',
@@ -47,12 +57,12 @@ describe('formNodeUtils', () => {
}, },
]; ];
executeFunctions.getNodeParameter.calledWith('formFields.values').mockReturnValue(formFields); webhookFunctions.getNodeParameter.calledWith('formFields.values').mockReturnValue(formFields);
const responseMock = mock<Response>({ render: mockRender } as any); const responseMock = mock<Response>({ render: mockRender } as any);
const triggerMock = mock<NodeTypeAndVersion>({ name: 'triggerName' } as any); const triggerMock = mock<NodeTypeAndVersion>({ name: 'triggerName' } as any);
await renderFormNode(executeFunctions, responseMock, triggerMock, formFields, 'test'); await renderFormNode(webhookFunctions, responseMock, triggerMock, formFields, 'test');
expect(mockRender).toHaveBeenCalledWith('form-trigger', { expect(mockRender).toHaveBeenCalledWith('form-trigger', {
appendAttribution: true, appendAttribution: true,

View File

@@ -17,6 +17,7 @@ import {
isFormConnected, isFormConnected,
sanitizeHtml, sanitizeHtml,
validateResponseModeConfiguration, validateResponseModeConfiguration,
prepareFormFields,
} from '../utils'; } from '../utils';
describe('FormTrigger, parseFormDescription', () => { describe('FormTrigger, parseFormDescription', () => {
@@ -994,4 +995,40 @@ describe('validateResponseModeConfiguration', () => {
expect(() => validateResponseModeConfiguration(webhookFunctions)).not.toThrow(); expect(() => validateResponseModeConfiguration(webhookFunctions)).not.toThrow();
}); });
describe('prepareFormFields', () => {
it('should resolve expressions in html fields', async () => {
webhookFunctions.evaluateExpression.mockImplementation((expression) => {
if (expression === '{{ $json.formMode }}') {
return 'Title';
}
});
const result = prepareFormFields(webhookFunctions, [
{
fieldLabel: 'Custom HTML',
fieldType: 'html',
elementName: 'test',
html: '<h1>{{ $json.formMode }}</h1>',
},
]);
expect(result[0].html).toBe('<h1>Title</h1>');
});
it('should prepare hiddenField', async () => {
const result = prepareFormFields(webhookFunctions, [
{
fieldLabel: '',
fieldName: 'test',
fieldType: 'hiddenField',
},
]);
expect(result[0]).toEqual({
fieldLabel: 'test',
fieldName: 'test',
fieldType: 'hiddenField',
});
});
});
}); });

View File

@@ -72,6 +72,28 @@ export function sanitizeHtml(text: string) {
}); });
} }
export const prepareFormFields = (context: IWebhookFunctions, fields: FormFieldsParameter) => {
return fields.map((field) => {
if (field.fieldType === 'html') {
let { html } = field;
if (!html) return field;
for (const resolvable of getResolvables(html)) {
html = html.replace(resolvable, context.evaluateExpression(resolvable) as string);
}
field.html = sanitizeHtml(html as string);
}
if (field.fieldType === 'hiddenField') {
field.fieldLabel = field.fieldName as string;
}
return field;
});
};
export function sanitizeCustomCss(css: string | undefined): string | undefined { export function sanitizeCustomCss(css: string | undefined): string | undefined {
if (!css) return undefined; if (!css) return undefined;
@@ -411,6 +433,8 @@ export function renderForm({
} catch (error) {} } catch (error) {}
} }
formFields = prepareFormFields(context, formFields);
const data = prepareFormData({ const data = prepareFormData({
formTitle, formTitle,
formDescription, formDescription,
@@ -476,17 +500,8 @@ export async function formWebhook(
} }
const mode = context.getMode() === 'manual' ? 'test' : 'production'; const mode = context.getMode() === 'manual' ? 'test' : 'production';
const formFields = (context.getNodeParameter('formFields.values', []) as FormFieldsParameter).map( const formFields = context.getNodeParameter('formFields.values', []) as FormFieldsParameter;
(field) => {
if (field.fieldType === 'html') {
field.html = sanitizeHtml(field.html as string);
}
if (field.fieldType === 'hiddenField') {
field.fieldLabel = field.fieldName as string;
}
return field;
},
);
const method = context.getRequestObject().method; const method = context.getRequestObject().method;
validateResponseModeConfiguration(context); validateResponseModeConfiguration(context);