diff --git a/packages/cli/templates/form-trigger.handlebars b/packages/cli/templates/form-trigger.handlebars
index ee868b072f..695ef79472 100644
--- a/packages/cli/templates/form-trigger.handlebars
+++ b/packages/cli/templates/form-trigger.handlebars
@@ -2,6 +2,11 @@
diff --git a/packages/nodes-base/nodes/Form/common.descriptions.ts b/packages/nodes-base/nodes/Form/common.descriptions.ts
index 299284c59f..dc0d8bf076 100644
--- a/packages/nodes-base/nodes/Form/common.descriptions.ts
+++ b/packages/nodes-base/nodes/Form/common.descriptions.ts
@@ -29,7 +29,7 @@ export const formDescription: INodeProperties = {
default: '',
placeholder: "e.g. We'll get back to you soon",
description:
- 'Shown underneath the Form Title. Can be used to prompt the user on how to complete the form.',
+ 'Shown underneath the Form Title. Can be used to prompt the user on how to complete the form. Accepts HTML.',
typeOptions: {
rows: 2,
},
diff --git a/packages/nodes-base/nodes/Form/interfaces.ts b/packages/nodes-base/nodes/Form/interfaces.ts
index 1cf5f64c92..b04d30d1d3 100644
--- a/packages/nodes-base/nodes/Form/interfaces.ts
+++ b/packages/nodes-base/nodes/Form/interfaces.ts
@@ -22,6 +22,7 @@ export type FormTriggerData = {
validForm: boolean;
formTitle: string;
formDescription?: string;
+ formDescriptionMetadata?: string;
formSubmittedHeader?: string;
formSubmittedText?: string;
redirectUrl?: string;
diff --git a/packages/nodes-base/nodes/Form/test/FormTriggerV2.node.test.ts b/packages/nodes-base/nodes/Form/test/FormTriggerV2.node.test.ts
index 63dbfd4986..9c69179066 100644
--- a/packages/nodes-base/nodes/Form/test/FormTriggerV2.node.test.ts
+++ b/packages/nodes-base/nodes/Form/test/FormTriggerV2.node.test.ts
@@ -50,6 +50,7 @@ describe('FormTrigger', () => {
appendAttribution: false,
buttonLabel: 'Submit',
formDescription: 'Test Description',
+ formDescriptionMetadata: 'Test Description',
formFields: [
{
defaultValue: '',
diff --git a/packages/nodes-base/nodes/Form/test/utils.test.ts b/packages/nodes-base/nodes/Form/test/utils.test.ts
index b37a3993e8..65decaa285 100644
--- a/packages/nodes-base/nodes/Form/test/utils.test.ts
+++ b/packages/nodes-base/nodes/Form/test/utils.test.ts
@@ -10,19 +10,58 @@ import type {
import {
formWebhook,
+ createDescriptionMetadata,
prepareFormData,
prepareFormReturnItem,
resolveRawData,
isFormConnected,
} from '../utils';
+describe('FormTrigger, parseFormDescription', () => {
+ it('should remove HTML tags and truncate to 150 characters', () => {
+ const descriptions = [
+ { description: '
This is a test description
', expected: 'This is a test description' },
+ { description: 'Test description', expected: 'Test description' },
+ {
+ description:
+ 'Beneath the golden hues of a setting sun, waves crashed against the rugged shore, carrying whispers of ancient tales etched in natures timeless and soothing song.',
+ expected:
+ 'Beneath the golden hues of a setting sun, waves crashed against the rugged shore, carrying whispers of ancient tales etched in natures timeless and so',
+ },
+ {
+ description:
+ '
Beneath the golden hues of a setting sun, waves crashed against the rugged shore, carrying whispers of ancient tales etched in natures timeless and soothing song.
',
+ expected:
+ 'Beneath the golden hues of a setting sun, waves crashed against the rugged shore, carrying whispers of ancient tales etched in natures timeless and so',
+ },
+ ];
+
+ descriptions.forEach(({ description, expected }) => {
+ expect(createDescriptionMetadata(description)).toBe(expected);
+ });
+ });
+});
+
describe('FormTrigger, formWebhook', () => {
+ const executeFunctions = mock
();
+ executeFunctions.getNode.mockReturnValue({ typeVersion: 2.1 } as any);
+ executeFunctions.getNodeParameter.calledWith('options').mockReturnValue({});
+ executeFunctions.getNodeParameter.calledWith('formTitle').mockReturnValue('Test Form');
+ executeFunctions.getNodeParameter
+ .calledWith('formDescription')
+ .mockReturnValue('Test Description');
+ executeFunctions.getNodeParameter.calledWith('responseMode').mockReturnValue('onReceived');
+ executeFunctions.getRequestObject.mockReturnValue({ method: 'GET', query: {} } as any);
+ executeFunctions.getMode.mockReturnValue('manual');
+ executeFunctions.getInstanceId.mockReturnValue('instanceId');
+ executeFunctions.getBodyData.mockReturnValue({ data: {}, files: {} });
+ executeFunctions.getChildNodes.mockReturnValue([]);
+
beforeEach(() => {
jest.clearAllMocks();
});
it('should call response render', async () => {
- const executeFunctions = mock();
const mockRender = jest.fn();
const formFields: FormFieldsParameter = [
@@ -43,20 +82,8 @@ describe('FormTrigger, formWebhook', () => {
},
];
- executeFunctions.getNode.mockReturnValue({ typeVersion: 2.1 } as any);
- executeFunctions.getNodeParameter.calledWith('options').mockReturnValue({});
- executeFunctions.getNodeParameter.calledWith('formTitle').mockReturnValue('Test Form');
- executeFunctions.getNodeParameter
- .calledWith('formDescription')
- .mockReturnValue('Test Description');
- executeFunctions.getNodeParameter.calledWith('responseMode').mockReturnValue('onReceived');
executeFunctions.getNodeParameter.calledWith('formFields.values').mockReturnValue(formFields);
executeFunctions.getResponseObject.mockReturnValue({ render: mockRender } as any);
- executeFunctions.getRequestObject.mockReturnValue({ method: 'GET', query: {} } as any);
- executeFunctions.getMode.mockReturnValue('manual');
- executeFunctions.getInstanceId.mockReturnValue('instanceId');
- executeFunctions.getBodyData.mockReturnValue({ data: {}, files: {} });
- executeFunctions.getChildNodes.mockReturnValue([]);
await formWebhook(executeFunctions);
@@ -64,6 +91,7 @@ describe('FormTrigger, formWebhook', () => {
appendAttribution: true,
buttonLabel: 'Submit',
formDescription: 'Test Description',
+ formDescriptionMetadata: 'Test Description',
formFields: [
{
defaultValue: '',
@@ -117,8 +145,55 @@ describe('FormTrigger, formWebhook', () => {
});
});
+ it('should sanitize form descriptions', async () => {
+ const mockRender = jest.fn();
+
+ const formDescription = [
+ { description: 'Test Description', expected: 'Test Description' },
+ { description: 'hello', expected: 'hello' },
+ { description: '', expected: '' },
+ ];
+ const formFields: FormFieldsParameter = [
+ { fieldLabel: 'Name', fieldType: 'text', requiredField: true },
+ ];
+
+ executeFunctions.getNodeParameter.calledWith('formFields.values').mockReturnValue(formFields);
+ executeFunctions.getResponseObject.mockReturnValue({ render: mockRender } as any);
+
+ for (const { description, expected } of formDescription) {
+ executeFunctions.getNodeParameter.calledWith('formDescription').mockReturnValue(description);
+
+ await formWebhook(executeFunctions);
+
+ expect(mockRender).toHaveBeenCalledWith('form-trigger', {
+ appendAttribution: true,
+ buttonLabel: 'Submit',
+ formDescription: expected,
+ formDescriptionMetadata: createDescriptionMetadata(expected),
+ formFields: [
+ {
+ defaultValue: '',
+ errorId: 'error-field-0',
+ id: 'field-0',
+ inputRequired: 'form-required',
+ isInput: true,
+ label: 'Name',
+ placeholder: undefined,
+ type: 'text',
+ },
+ ],
+ formSubmittedText: 'Your response has been recorded',
+ formTitle: 'Test Form',
+ n8nWebsiteLink:
+ 'https://n8n.io/?utm_source=n8n-internal&utm_medium=form-trigger&utm_campaign=instanceId',
+ testRun: true,
+ useResponseData: false,
+ validForm: true,
+ });
+ }
+ });
+
it('should return workflowData on POST request', async () => {
- const executeFunctions = mock();
const mockStatus = jest.fn();
const mockEnd = jest.fn();
@@ -132,15 +207,9 @@ describe('FormTrigger, formWebhook', () => {
'field-1': '30',
};
- executeFunctions.getNode.mockReturnValue({ typeVersion: 2.1 } as any);
- executeFunctions.getNodeParameter.calledWith('options').mockReturnValue({});
- executeFunctions.getNodeParameter.calledWith('responseMode').mockReturnValue('onReceived');
- executeFunctions.getChildNodes.mockReturnValue([]);
executeFunctions.getNodeParameter.calledWith('formFields.values').mockReturnValue(formFields);
executeFunctions.getResponseObject.mockReturnValue({ status: mockStatus, end: mockEnd } as any);
executeFunctions.getRequestObject.mockReturnValue({ method: 'POST' } as any);
- executeFunctions.getMode.mockReturnValue('manual');
- executeFunctions.getInstanceId.mockReturnValue('instanceId');
executeFunctions.getBodyData.mockReturnValue({ data: bodyData, files: {} });
const result = await formWebhook(executeFunctions);
@@ -213,6 +282,7 @@ describe('FormTrigger, prepareFormData', () => {
validForm: true,
formTitle: 'Test Form',
formDescription: 'This is a test form',
+ formDescriptionMetadata: 'This is a test form',
formSubmittedText: 'Thank you for your submission',
n8nWebsiteLink:
'https://n8n.io/?utm_source=n8n-internal&utm_medium=form-trigger&utm_campaign=test-instance',
@@ -292,6 +362,7 @@ describe('FormTrigger, prepareFormData', () => {
validForm: true,
formTitle: 'Test Form',
formDescription: 'This is a test form',
+ formDescriptionMetadata: 'This is a test form',
formSubmittedText: 'Your response has been recorded',
n8nWebsiteLink: 'https://n8n.io/?utm_source=n8n-internal&utm_medium=form-trigger',
formFields: [
diff --git a/packages/nodes-base/nodes/Form/utils.ts b/packages/nodes-base/nodes/Form/utils.ts
index 6fc414f622..e4b46c72fd 100644
--- a/packages/nodes-base/nodes/Form/utils.ts
+++ b/packages/nodes-base/nodes/Form/utils.ts
@@ -16,6 +16,7 @@ import {
WAIT_NODE_TYPE,
jsonParse,
} from 'n8n-workflow';
+import sanitize from 'sanitize-html';
import type { FormTriggerData, FormTriggerInput } from './interfaces';
import { FORM_TRIGGER_AUTHENTICATION_PROPERTY } from './interfaces';
@@ -23,6 +24,41 @@ import { getResolvables } from '../../utils/utilities';
import { WebhookAuthorizationError } from '../Webhook/error';
import { validateWebhookAuthentication } from '../Webhook/utils';
+function sanitizeHtml(text: string) {
+ return sanitize(text, {
+ allowedTags: [
+ 'b',
+ 'i',
+ 'em',
+ 'strong',
+ 'a',
+ 'h1',
+ 'h2',
+ 'h3',
+ 'h4',
+ 'h5',
+ 'h6',
+ 'u',
+ 'sub',
+ 'sup',
+ 'code',
+ 'pre',
+ 'span',
+ 'br',
+ ],
+ allowedAttributes: {
+ a: ['href', 'target', 'rel'],
+ },
+ nonBooleanAttributes: ['*'],
+ });
+}
+
+export function createDescriptionMetadata(description: string) {
+ return description === ''
+ ? 'n8n form'
+ : description.replace(/^\s*\n+|<\/?[^>]+(>|$)/g, '').slice(0, 150);
+}
+
export function prepareFormData({
formTitle,
formDescription,
@@ -63,6 +99,7 @@ export function prepareFormData({
validForm,
formTitle,
formDescription,
+ formDescriptionMetadata: createDescriptionMetadata(formDescription),
formSubmittedHeader,
formSubmittedText,
n8nWebsiteLink,
@@ -380,7 +417,7 @@ export async function formWebhook(
//Show the form on GET request
if (method === 'GET') {
const formTitle = context.getNodeParameter('formTitle', '') as string;
- const formDescription = context.getNodeParameter('formDescription', '') as string;
+ const formDescription = sanitizeHtml(context.getNodeParameter('formDescription', '') as string);
const responseMode = context.getNodeParameter('responseMode', '') as string;
let formSubmittedText;
diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json
index 9a42eae713..309d93c310 100644
--- a/packages/nodes-base/package.json
+++ b/packages/nodes-base/package.json
@@ -841,6 +841,7 @@
"@types/nodemailer": "^6.4.14",
"@types/promise-ftp": "^1.3.4",
"@types/rfc2047": "^2.0.1",
+ "@types/sanitize-html": "^2.11.0",
"@types/showdown": "^1.9.4",
"@types/snowflake-sdk": "^1.6.24",
"@types/ssh2-sftp-client": "^5.1.0",
@@ -906,6 +907,7 @@
"rhea": "1.0.24",
"rrule": "2.8.1",
"rss-parser": "3.13.0",
+ "sanitize-html": "2.12.1",
"semver": "7.5.4",
"showdown": "2.1.0",
"simple-git": "3.17.0",
diff --git a/packages/nodes-base/utils/sendAndWait/test/util.test.ts b/packages/nodes-base/utils/sendAndWait/test/util.test.ts
index 5194887f2f..39a6f16859 100644
--- a/packages/nodes-base/utils/sendAndWait/test/util.test.ts
+++ b/packages/nodes-base/utils/sendAndWait/test/util.test.ts
@@ -240,6 +240,7 @@ describe('Send and Wait utils tests', () => {
validForm: true,
formTitle: '',
formDescription: 'Test message',
+ formDescriptionMetadata: 'Test message',
formSubmittedHeader: 'Got it, thanks',
formSubmittedText: 'This page can be closed now',
n8nWebsiteLink: 'https://n8n.io/?utm_source=n8n-internal&utm_medium=form-trigger',
@@ -318,6 +319,7 @@ describe('Send and Wait utils tests', () => {
validForm: true,
formTitle: 'Test title',
formDescription: 'Test description',
+ formDescriptionMetadata: 'Test description',
formSubmittedHeader: 'Got it, thanks',
formSubmittedText: 'This page can be closed now',
n8nWebsiteLink: 'https://n8n.io/?utm_source=n8n-internal&utm_medium=form-trigger',
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 593b2451d1..af9b0caa4d 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1851,6 +1851,9 @@ importers:
rss-parser:
specifier: 3.13.0
version: 3.13.0
+ sanitize-html:
+ specifier: 2.12.1
+ version: 2.12.1
semver:
specifier: ^7.5.4
version: 7.6.0
@@ -1936,6 +1939,9 @@ importers:
'@types/rfc2047':
specifier: ^2.0.1
version: 2.0.1
+ '@types/sanitize-html':
+ specifier: ^2.11.0
+ version: 2.11.0
'@types/showdown':
specifier: ^1.9.4
version: 1.9.4