1
0
Fork 0
mirror of https://github.com/n8n-io/n8n.git synced 2025-03-05 20:50:17 -08:00

fix(HTML Node): Escape data path value in JSON Property ()

This commit is contained in:
Michael Kret 2024-01-26 11:51:03 +00:00 committed by GitHub
parent 7c49004018
commit fc5c562785
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 152 additions and 108 deletions
packages/nodes-base
nodes
Html
Set/v2/helpers
utils

View file

@ -5,12 +5,15 @@ import type {
INodeType, INodeType,
INodeTypeDescription, INodeTypeDescription,
IDataObject, IDataObject,
INodeProperties,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow'; import { NodeOperationError } from 'n8n-workflow';
import { placeholder } from './placeholder'; import { placeholder } from './placeholder';
import { getValue } from './utils'; import { getValue } from './utils';
import type { IValueData } from './types'; import type { IValueData } from './types';
import { getResolvables } from '@utils/utilities'; import { getResolvables, sanitazeDataPathKey } from '@utils/utilities';
import get from 'lodash/get';
export const capitalizeHeader = (header: string, capitalize?: boolean) => { export const capitalizeHeader = (header: string, capitalize?: boolean) => {
if (!capitalize) return header; if (!capitalize) return header;
@ -21,13 +24,97 @@ export const capitalizeHeader = (header: string, capitalize?: boolean) => {
.join(' '); .join(' ');
}; };
const extractionValuesCollection: INodeProperties = {
displayName: 'Extraction Values',
name: 'extractionValues',
placeholder: 'Add Value',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
options: [
{
name: 'values',
displayName: 'Values',
values: [
{
displayName: 'Key',
name: 'key',
type: 'string',
default: '',
description: 'The key under which the extracted value should be saved',
},
{
displayName: 'CSS Selector',
name: 'cssSelector',
type: 'string',
default: '',
placeholder: '.price',
description: 'The CSS selector to use',
},
{
displayName: 'Return Value',
name: 'returnValue',
type: 'options',
options: [
{
name: 'Attribute',
value: 'attribute',
description: 'Get an attribute value like "class" from an element',
},
{
name: 'HTML',
value: 'html',
description: 'Get the HTML the element contains',
},
{
name: 'Text',
value: 'text',
description: 'Get only the text content of the element',
},
{
name: 'Value',
value: 'value',
description: 'Get value of an input, select or textarea',
},
],
default: 'text',
description: 'What kind of data should be returned',
},
{
displayName: 'Attribute',
name: 'attribute',
type: 'string',
displayOptions: {
show: {
returnValue: ['attribute'],
},
},
default: '',
placeholder: 'class',
description: 'The name of the attribute to return the value off',
},
{
displayName: 'Return Array',
name: 'returnArray',
type: 'boolean',
default: false,
description:
'Whether to return the values as an array so if multiple ones get found they also get returned separately. If not set all will be returned as a single string.',
},
],
},
],
};
export class Html implements INodeType { export class Html implements INodeType {
description: INodeTypeDescription = { description: INodeTypeDescription = {
displayName: 'HTML', displayName: 'HTML',
name: 'html', name: 'html',
icon: 'file:html.svg', icon: 'file:html.svg',
group: ['transform'], group: ['transform'],
version: 1, version: [1, 1.1],
subtitle: '={{ $parameter["operation"] }}', subtitle: '={{ $parameter["operation"] }}',
description: 'Work with HTML', description: 'Work with HTML',
defaults: { defaults: {
@ -143,94 +230,33 @@ export class Html implements INodeType {
'Name of the JSON property in which the HTML to extract the data from can be found. The property can either contain a string or an array of strings.', 'Name of the JSON property in which the HTML to extract the data from can be found. The property can either contain a string or an array of strings.',
}, },
{ {
displayName: 'Extraction Values', ...extractionValuesCollection,
name: 'extractionValues', displayOptions: {
placeholder: 'Add Value', show: {
type: 'fixedCollection', operation: ['extractHtmlContent'],
typeOptions: { '@version': [1],
multipleValues: true, },
},
},
{
...extractionValuesCollection,
default: {
values: [
{
key: '',
cssSelector: '',
returnValue: 'text',
returnArray: false,
},
],
}, },
displayOptions: { displayOptions: {
show: { show: {
operation: ['extractHtmlContent'], operation: ['extractHtmlContent'],
'@version': [{ _cnd: { gt: 1 } }],
}, },
}, },
default: {},
options: [
{
name: 'values',
displayName: 'Values',
values: [
{
displayName: 'Key',
name: 'key',
type: 'string',
default: '',
description: 'The key under which the extracted value should be saved',
},
{
displayName: 'CSS Selector',
name: 'cssSelector',
type: 'string',
default: '',
placeholder: '.price',
description: 'The CSS selector to use',
},
{
displayName: 'Return Value',
name: 'returnValue',
type: 'options',
options: [
{
name: 'Attribute',
value: 'attribute',
description: 'Get an attribute value like "class" from an element',
},
{
name: 'HTML',
value: 'html',
description: 'Get the HTML the element contains',
},
{
name: 'Text',
value: 'text',
description: 'Get only the text content of the element',
},
{
name: 'Value',
value: 'value',
description: 'Get value of an input, select or textarea',
},
],
default: 'text',
description: 'What kind of data should be returned',
},
{
displayName: 'Attribute',
name: 'attribute',
type: 'string',
displayOptions: {
show: {
returnValue: ['attribute'],
},
},
default: '',
placeholder: 'class',
description: 'The name of the attribute to return the value off',
},
{
displayName: 'Return Array',
name: 'returnArray',
type: 'boolean',
default: false,
description:
'Whether to return the values as an array so if multiple ones get found they also get returned separately. If not set all will be returned as a single string.',
},
],
},
],
}, },
{ {
displayName: 'Options', displayName: 'Options',
name: 'options', name: 'options',
@ -329,6 +355,7 @@ export class Html implements INodeType {
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> { async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData(); const items = this.getInputData();
const operation = this.getNodeParameter('operation', 0); const operation = this.getNodeParameter('operation', 0);
const nodeVersion = this.getNode().typeVersion;
if (operation === 'convertToHtmlTable' && items.length) { if (operation === 'convertToHtmlTable' && items.length) {
let table = ''; let table = '';
@ -467,14 +494,27 @@ export class Html implements INodeType {
let htmlArray: string[] | string = []; let htmlArray: string[] | string = [];
if (sourceData === 'json') { if (sourceData === 'json') {
if (item.json[dataPropertyName] === undefined) { if (nodeVersion === 1) {
throw new NodeOperationError( const key = sanitazeDataPathKey(item.json, dataPropertyName);
this.getNode(), if (item.json[key] === undefined) {
`No property named "${dataPropertyName}" exists!`, throw new NodeOperationError(
{ itemIndex }, this.getNode(),
); `No property named "${dataPropertyName}" exists!`,
{ itemIndex },
);
}
htmlArray = item.json[key] as string;
} else {
const value = get(item.json, dataPropertyName);
if (value === undefined) {
throw new NodeOperationError(
this.getNode(),
`No property named "${dataPropertyName}" exists!`,
{ itemIndex },
);
}
htmlArray = value as string;
} }
htmlArray = item.json[dataPropertyName] as string;
} else { } else {
this.helpers.assertBinaryData(itemIndex, dataPropertyName); this.helpers.assertBinaryData(itemIndex, dataPropertyName);
const binaryDataBuffer = await this.helpers.getBinaryDataBuffer( const binaryDataBuffer = await this.helpers.getBinaryDataBuffer(
@ -489,7 +529,7 @@ export class Html implements INodeType {
htmlArray = [htmlArray]; htmlArray = [htmlArray];
} }
for (const html of htmlArray as string[]) { for (const html of htmlArray) {
const $ = cheerio.load(html); const $ = cheerio.load(html);
const newItem: INodeExecutionData = { const newItem: INodeExecutionData = {

View file

@ -17,7 +17,7 @@ import set from 'lodash/set';
import get from 'lodash/get'; import get from 'lodash/get';
import unset from 'lodash/unset'; import unset from 'lodash/unset';
import { getResolvables } from '../../../../utils/utilities'; import { getResolvables, sanitazeDataPathKey } from '../../../../utils/utilities';
import type { SetNodeOptions, SetField } from './interfaces'; import type { SetNodeOptions, SetField } from './interfaces';
import { INCLUDE } from './interfaces'; import { INCLUDE } from './interfaces';
@ -35,28 +35,15 @@ const configureFieldHelper = (dotNotation?: boolean) => {
}, },
}; };
} else { } else {
const sanitazeKey = (item: IDataObject, key: string) => {
if (item[key] !== undefined) {
return key;
}
if (key.startsWith("['") && key.endsWith("']")) {
key = key.slice(2, -2);
if (item[key] !== undefined) {
return key;
}
}
return key;
};
return { return {
set: (item: IDataObject, key: string, value: IDataObject) => { set: (item: IDataObject, key: string, value: IDataObject) => {
item[sanitazeKey(item, key)] = value; item[sanitazeDataPathKey(item, key)] = value;
}, },
get: (item: IDataObject, key: string) => { get: (item: IDataObject, key: string) => {
return item[sanitazeKey(item, key)]; return item[sanitazeDataPathKey(item, key)];
}, },
unset: (item: IDataObject, key: string) => { unset: (item: IDataObject, key: string) => {
delete item[sanitazeKey(item, key)]; delete item[sanitazeDataPathKey(item, key)];
}, },
}; };
} }

View file

@ -326,3 +326,20 @@ export function preparePairedItemDataArray(
if (Array.isArray(pairedItem)) return pairedItem; if (Array.isArray(pairedItem)) return pairedItem;
return [pairedItem]; return [pairedItem];
} }
export const sanitazeDataPathKey = (item: IDataObject, key: string) => {
if (item[key] !== undefined) {
return key;
}
if (
(key.startsWith("['") && key.endsWith("']")) ||
(key.startsWith('["') && key.endsWith('"]'))
) {
key = key.slice(2, -2);
if (item[key] !== undefined) {
return key;
}
}
return key;
};