diff --git a/packages/cli/templates/form-trigger.handlebars b/packages/cli/templates/form-trigger.handlebars
index 695ef79472..6bad1a02d8 100644
--- a/packages/cli/templates/form-trigger.handlebars
+++ b/packages/cli/templates/form-trigger.handlebars
@@ -371,6 +371,12 @@
{{/if}}
+ {{#if isHtml}}
+
diff --git a/packages/nodes-base/nodes/Form/common.descriptions.ts b/packages/nodes-base/nodes/Form/common.descriptions.ts
index dc0d8bf076..67cf3515d1 100644
--- a/packages/nodes-base/nodes/Form/common.descriptions.ts
+++ b/packages/nodes-base/nodes/Form/common.descriptions.ts
@@ -2,6 +2,12 @@ import type { INodeProperties } from 'n8n-workflow';
import { appendAttributionOption } from '../../utils/descriptions';
+export const placeholder: string = `
+
+
+
+`.trimStart();
+
export const webhookPath: INodeProperties = {
displayName: 'Form Path',
name: 'path',
@@ -36,9 +42,9 @@ export const formDescription: INodeProperties = {
};
export const formFields: INodeProperties = {
- displayName: 'Form Fields',
+ displayName: 'Form Elements',
name: 'formFields',
- placeholder: 'Add Form Field',
+ placeholder: 'Add Form Element',
type: 'fixedCollection',
default: { values: [{ label: '', fieldType: 'text' }] },
typeOptions: {
@@ -60,12 +66,16 @@ export const formFields: INodeProperties = {
required: true,
},
{
- displayName: 'Field Type',
+ displayName: 'Element Type',
name: 'fieldType',
type: 'options',
default: 'text',
description: 'The type of field to add to the form',
options: [
+ {
+ name: 'Custom HTML',
+ value: 'html',
+ },
{
name: 'Date',
value: 'date',
@@ -109,7 +119,7 @@ export const formFields: INodeProperties = {
default: '',
displayOptions: {
hide: {
- fieldType: ['dropdown', 'date', 'file'],
+ fieldType: ['dropdown', 'date', 'file', 'html'],
},
},
},
@@ -158,6 +168,21 @@ export const formFields: INodeProperties = {
},
},
},
+ {
+ displayName: 'HTML Template',
+ name: 'html',
+ typeOptions: {
+ editor: 'htmlEditor',
+ },
+ type: 'string',
+ default: placeholder,
+ description: 'HTML template to render',
+ displayOptions: {
+ show: {
+ fieldType: ['html'],
+ },
+ },
+ },
{
displayName: 'Multiple Files',
name: 'multipleFiles',
@@ -190,6 +215,23 @@ export const formFields: INodeProperties = {
name: 'formatDate',
type: 'notice',
default: '',
+ displayOptions: {
+ show: {
+ fieldType: ['date'],
+ },
+ },
+ },
+ {
+ displayName:
+ 'Does not accept
<style>
<script>
or
<input>
tags.',
+ name: 'htmlTips',
+ type: 'notice',
+ default: '',
+ displayOptions: {
+ show: {
+ fieldType: ['html'],
+ },
+ },
},
{
displayName: 'Required Field',
@@ -198,6 +240,11 @@ export const formFields: INodeProperties = {
default: false,
description:
'Whether to require the user to enter a value for this field before submitting the form',
+ displayOptions: {
+ hide: {
+ fieldType: ['html'],
+ },
+ },
},
],
},
diff --git a/packages/nodes-base/nodes/Form/test/utils.test.ts b/packages/nodes-base/nodes/Form/test/utils.test.ts
index 65decaa285..4d1bf39c36 100644
--- a/packages/nodes-base/nodes/Form/test/utils.test.ts
+++ b/packages/nodes-base/nodes/Form/test/utils.test.ts
@@ -15,6 +15,7 @@ import {
prepareFormReturnItem,
resolveRawData,
isFormConnected,
+ sanitizeHtml,
} from '../utils';
describe('FormTrigger, parseFormDescription', () => {
@@ -42,6 +43,29 @@ describe('FormTrigger, parseFormDescription', () => {
});
});
+describe('FormTrigger, sanitizeHtml', () => {
+ it('should remove forbidden HTML tags', () => {
+ const givenHtml = [
+ {
+ html: '',
+ expected: '',
+ },
+ {
+ html: '',
+ expected: '',
+ },
+ {
+ html: '
',
+ expected: '',
+ },
+ ];
+
+ givenHtml.forEach(({ html, expected }) => {
+ expect(sanitizeHtml(html)).toBe(expected);
+ });
+ });
+});
+
describe('FormTrigger, formWebhook', () => {
const executeFunctions = mock
();
executeFunctions.getNode.mockReturnValue({ typeVersion: 2.1 } as any);
@@ -80,6 +104,12 @@ describe('FormTrigger, formWebhook', () => {
acceptFileTypes: '.pdf,.doc',
multipleFiles: false,
},
+ {
+ fieldLabel: 'Custom HTML',
+ fieldType: 'html',
+ html: 'Test HTML
',
+ requiredField: false,
+ },
];
executeFunctions.getNodeParameter.calledWith('formFields.values').mockReturnValue(formFields);
@@ -134,6 +164,16 @@ describe('FormTrigger, formWebhook', () => {
multipleFiles: '',
placeholder: undefined,
},
+ {
+ id: 'field-4',
+ errorId: 'error-field-4',
+ label: 'Custom HTML',
+ inputRequired: '',
+ defaultValue: '',
+ placeholder: undefined,
+ html: 'Test HTML
',
+ isHtml: true,
+ },
],
formSubmittedText: 'Your response has been recorded',
formTitle: 'Test Form',
diff --git a/packages/nodes-base/nodes/Form/utils.ts b/packages/nodes-base/nodes/Form/utils.ts
index e4b46c72fd..2510be56ac 100644
--- a/packages/nodes-base/nodes/Form/utils.ts
+++ b/packages/nodes-base/nodes/Form/utils.ts
@@ -24,11 +24,16 @@ import { getResolvables } from '../../utils/utilities';
import { WebhookAuthorizationError } from '../Webhook/error';
import { validateWebhookAuthentication } from '../Webhook/utils';
-function sanitizeHtml(text: string) {
+export function sanitizeHtml(text: string) {
return sanitize(text, {
allowedTags: [
'b',
+ 'div',
'i',
+ 'iframe',
+ 'img',
+ 'video',
+ 'source',
'em',
'strong',
'a',
@@ -48,8 +53,18 @@ function sanitizeHtml(text: string) {
],
allowedAttributes: {
a: ['href', 'target', 'rel'],
+ img: ['src', 'alt', 'width', 'height'],
+ video: ['*'],
+ iframe: ['*'],
+ source: ['*'],
+ },
+ transformTags: {
+ iframe: sanitize.simpleTransform('iframe', {
+ sandbox: '',
+ referrerpolicy: 'strict-origin-when-cross-origin',
+ allow: 'fullscreen; autoplay; encrypted-media',
+ }),
},
- nonBooleanAttributes: ['*'],
});
}
@@ -149,6 +164,9 @@ export function prepareFormData({
input.selectOptions = fieldOptions.map((e) => e.option);
} else if (fieldType === 'textarea') {
input.isTextarea = true;
+ } else if (fieldType === 'html') {
+ input.isHtml = true;
+ input.html = field.html as string;
} else {
input.isInput = true;
input.type = fieldType as 'text' | 'number' | 'date' | 'email';
@@ -409,7 +427,14 @@ export async function formWebhook(
}
const mode = context.getMode() === 'manual' ? 'test' : 'production';
- const formFields = context.getNodeParameter('formFields.values', []) as FormFieldsParameter;
+ const formFields = (context.getNodeParameter('formFields.values', []) as FormFieldsParameter).map(
+ (field) => {
+ if (field.fieldType === 'html') {
+ field.html = sanitizeHtml(field.html as string);
+ }
+ return field;
+ },
+ );
const method = context.getRequestObject().method;
checkResponseModeConfiguration(context);
diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts
index 10fc8e9b11..dd6c0ef1ce 100644
--- a/packages/workflow/src/Interfaces.ts
+++ b/packages/workflow/src/Interfaces.ts
@@ -2684,6 +2684,7 @@ export type FormFieldsParameter = Array<{
multipleFiles?: boolean;
acceptFileTypes?: string;
formatDate?: string;
+ html?: string;
placeholder?: string;
}>;